简介
事务是数据库将相关语句分组在一起,由系统作为单个单元执行的一种方式。这些语句作为一个单元进行处理,所有组成部分要么成功完成,要么恢复到其原始状态。这是在需要多个独立步骤才能实现单个目标的情况下保持一致性的一种方法。
在本文中,我们将讨论什么是事务以及它们如何有用。然后,我们将了解 MySQL 如何以各种方式使用事务来精确控制语句如何应用于数据库。
什么是事务?
事务是一种将多个语句分组并隔离起来,作为单个操作进行处理的方式。事务不是将每个命令单独发送到服务器并立即执行,而是将命令捆绑在一起并在与其他请求分离的上下文中执行。
隔离是事务的重要组成部分。在事务内部,执行的语句只能影响事务本身的环境。从事务内部看,语句可以修改数据,结果立即可见。从外部看,在事务提交之前,不会进行任何更改,此时事务内的所有操作将立即变得可见。
这些特性通过提供原子性(事务中的操作要么全部提交,要么全部回滚)和隔离性(在事务外部,在提交之前没有任何更改,而在事务内部,每个语句都会立即产生后果)来帮助数据库实现ACID 合规性。这些共同帮助数据库保持一致性(通过保证不会发生部分数据转换)。此外,事务中的更改在提交到非易失性存储之前不会被报告为成功,这提供了持久性。
为了实现这些目标,事务采用了许多不同的策略,不同的数据库系统使用不同的方法。MySQL 使用一种称为多版本并发控制 (MVCC) 的系统,该系统允许数据库使用数据快照执行这些操作。总而言之,这些系统构成了现代关系数据库的基本构建块之一,允许它们以防崩溃的方式安全地处理复杂数据。
一致性故障的类型
人们使用事务的一个原因是,它们可以对数据的一致性以及数据处理的环境提供某些保证。一致性可以通过许多不同的方式被破坏,这会影响数据库如何尝试防止它们。
根据事务实现的不同,不一致性可以通过四种主要方式产生。您对这些情况可能出现的场景的容忍度将影响您在应用程序中使用事务的方式。
脏读
脏读发生在事务内的语句能够读取其他正在进行的事务写入的数据时。这意味着即使事务的语句尚未提交,它们也可以被读取,从而影响其他事务。
这通常被认为是严重的一致性破坏,因为事务之间没有正确隔离。可能永远不会提交到数据库的语句会影响其他事务的执行,从而修改它们的行为。
允许脏读的事务无法对结果数据的一致性做出任何合理的声明。
不可重复读
不可重复读发生在事务外部的提交更改了事务内部看到的数据时。如果在事务内部,相同的数据被读取两次但每次检索到不同的值,则可以识别出此类问题。
与脏读一样,允许不可重复读的事务不能提供事务之间的完全隔离。区别在于,对于不可重复读,影响事务的语句实际上已在事务外部提交。
幻读
幻读是一种特殊类型的不可重复读,当查询返回的行在事务内第二次执行时不同时发生。
例如,如果事务内的查询第一次执行时返回四行,而第二次执行时返回五行,则这是幻读。幻读是由事务外部的提交更改满足查询的行数引起的。
串行化异常
串行化异常发生在多个并发提交的事务的结果与它们一个接一个提交的结果不同时。这可能发生在事务允许两个提交在不解决冲突的情况下修改同一表或数据时。
事务隔离级别
事务并非“一刀切”的解决方案。不同的场景需要在性能和保护之间进行不同的权衡。幸运的是,MySQL 允许您指定所需的事务隔离类型。
大多数数据库系统提供的隔离级别包括以下内容
读未提交
读未提交是隔离级别,它对保持数据一致性和隔离性提供的保证最少。虽然使用 read uncommitted 的事务具有一些与事务相关的常见特性,例如能够同时提交多个语句或在发生错误时回滚语句,但它们确实允许出现许多可能破坏一致性的情况。
配置了 read uncommitted 隔离级别的事务允许
- 脏读
- 不可重复读
- 幻读
- 串行化异常
不建议使用此隔离级别,因为它几乎不提供任何数据一致性和隔离保证。这主要在您需要以“全有或全无”的提交模型将语句分组在一起,但不需要任何完整性保证时才有用(这种情况很少发生)。
读已提交
读已提交是一种专门防止脏读的隔离级别。当事务使用 read committed 一致性级别时,未提交的数据永远不会影响事务的内部上下文。这通过确保未提交的数据永远不会影响事务来提供基本的一致性级别。
尽管 read committed 提供了比 read uncommitted 更大的保护,但它并不能防止所有类型的不一致。这些问题仍然可能出现
- 不可重复读
- 幻读
- 串行化异常
可重复读
可重复读隔离级别建立在 read committed 提供的保证之上。它像以前一样避免脏读,但也阻止不可重复读。
尽管 repeatable read 可以防止不可重复读和脏读,但它仍然可能受到以下隔离问题的影响
- 幻读
- 串行化异常
在大多数情况下,幻读也会被阻止,但在某些情况下它们仍然可能发生。在链接的示例中,事务中的 SELECT 查询不返回任何结果,即使在另一事务插入并提交一行之后也是如此。但是,发出一个会更新新行的查询会导致查询返回数据,即使更新不应该知道另一事务提交的行。此后,SELECT 查询会返回数据。
对于 MySQL 的 InnoDB 引擎(在大多数情况下是正常的引擎),可重复读隔离方法是默认设置。
串行化
可串行化隔离级别提供最高级别的隔离和一致性。它防止了 repeatable read 级别所允许的所有情况,同时还消除了串行化异常的可能性。
可串行化隔离保证并发事务的提交就好像它们是按顺序执行的一样。如果出现可能引入串行化异常的情况,其中一个事务将出现串行化失败,而不是向数据集引入不一致。
定义事务
现在我们已经介绍了 MySQL 在事务中可以使用的不同隔离级别,让我们演示如何定义事务。
在 MySQL 中,默认情况下,在显式标记的事务之外的每个语句实际上都在其自己的单语句事务中执行。要显式启动事务块,您可以使用 START TRANSACTION 或 BEGIN 命令。START TRANSACTION 形式可以带 BEGIN 形式不能带的修饰符,因此 MySQL 建议您使用 START TRANSACTION。要提交事务,请发出 COMMIT 命令。
因此,事务的基本语法如下所示
START TRANSACTION;statementsCOMMIT;
作为一个更具体的例子,假设我们试图将 1000 美元从一个账户转移到另一个账户。我们希望确保这笔钱始终在两个账户之一中,但绝不同时存在于两者中。
我们可以将这两个语句一起封装在一个事务中,如下所示
START TRANSACTION;UPDATE accountsSET balance = balance - 1000WHERE id = 1;UPDATE accountsSET balance = balance + 1000WHERE id = 2;COMMIT;
在这里,1000 美元不会从 id = 1 的账户中取出,除非也同时将 1000 美元存入 id = 2 的账户中。虽然这两个语句在事务内部是按顺序执行的,但它们将同时提交,从而在底层数据集上同时执行。
回滚事务
在事务内部,所有语句要么全部提交到数据库,要么全部不提交。放弃在事务内所做的语句和修改而不是将其应用于数据库称为“回滚”事务。
事务可以自动或手动回滚。如果事务中的某个语句导致错误或在其他场景中为避免问题,MySQL 会自动回滚事务。
要手动回滚当前事务期间给出的语句,可以使用 ROLLBACK 命令。这将取消事务中的所有语句,实质上将时间倒回到事务的开始。
例如,假设我们使用之前使用的相同银行账户示例,如果在发出 UPDATE 语句后发现我们意外地转账了错误的金额或使用了错误的账户,我们可以回滚更改而不是提交它们
START TRANSACTION;UPDATE accountsSET balance = balance - 1500WHERE id = 1;UPDATE accountsSET balance = balance + 1500WHERE id = 3; -- Wrong account number here! Must rollback/* Gets us back to where we were before the transaction started */ROLLBACK;
一旦我们 ROLLBACK,1500 美元仍将保留在 id = 1 的账户中。
回滚时使用保存点
默认情况下,ROLLBACK 命令将事务重置为最初调用 START TRANSACTION 或 BEGIN 命令时的状态。但是,如果我们只想还原事务中的某些语句怎么办?
虽然在发出 ROLLBACK 命令时不能指定任意回滚位置,但您可以回滚到您在整个事务中设置的任何“保存点”。您可以使用 SAVEPOINT 命令预先标记事务中的位置,然后在需要回滚时引用这些特定位置。
这些保存点允许您创建中间回滚点。然后,您可以选择性地恢复当前位置与保存点之间所做的任何语句,然后继续处理事务。
要指定保存点,请发出 SAVEPOINT 命令,后跟保存点的名称
SAVEPOINT save_1;
要回滚到该保存点,请使用 ROLLBACK TO 命令
ROLLBACK TO save_1;
让我们继续使用我们一直在使用的以账户为中心的例子
START TRANSACTION;UPDATE accountsSET balance = balance - 1500WHERE id = 1;/* Set a save point that we can return to */SAVEPOINT save_1;UPDATE accountsSET balance = balance + 1500WHERE id = 3; -- Wrong account number here! We can rollback to the save point though!/* Gets us back to the state of the transaction at `save_1` */ROLLBACK TO save_1;/* Continue the transaction with the correct account number */UPDATE accountsSET balance = balance + 1500WHERE id = 4;COMMIT;
在这里,我们能够从我们犯的错误中恢复,而不会丢失迄今为止我们在事务中所做的所有工作。回滚后,我们继续按计划使用正确的语句进行事务。
设置事务的隔离级别
要设置事务的隔离级别,您可以使用带有 ISOLATION LEVEL 子句的 SET TRANSACTION 语句。SET TRANSACTION 语句允许您修改事务的隔离级别以及读写权限。
默认情况下,SET TRANSACTION 语句仅影响下一个启动的事务的属性。它必须在 START TRANSACTION 或 BEGIN 语句之前给出。基本语法如下所示
SET TRANSACTION ISOLATION LEVEL <isolation_level>;START TRANSACTION;statementsCOMMIT;
<isolation_level> 可以是以下任何一种(前面已详细描述)
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READ(MySQL 的默认操作模式)SERIALIZABLE
您还可以在发出命令时提供关键字 GLOBAL 或 SESSION 以影响不同的范围。
如果您键入 SET GLOBAL TRANSACTION ISOLATION LEVEL,隔离级别将全局更改为所有未来的会话。任何当前会话仍将使用旧的隔离级别,因此如果您需要使用事务级别或 SESSION 级别更改这些会话,请务必明确修改这些范围。
SESSION 修饰符(如 SET SESSION TRANSACTION ISOLATION LEVEL)允许您更改同一会话中任何未来事务的隔离级别。同样,这不会影响任何现有事务。
您可以使用 SET TRANSACTION 修改的另一个方面是事务是否具有读/写能力或只读。默认情况下,MySQL 中的事务具有读写能力。您可以使用 SET TRANSACTION READ ONLY 将会话设置为只读。如果您想明确将会话设置为读/写,也可以发出 SET TRANSACTION READ WRITE。
事务链
如果您有多个应该按顺序执行的事务,您可以选择使用 COMMIT AND CHAIN 命令将它们链接在一起。
COMMIT AND CHAIN 命令通过提交其中的语句来完成当前事务。在处理提交后,它会立即以相同的隔离级别打开一个新事务。这允许您将另一组语句分组到一个事务中。
该语句的作用与您发出 COMMIT; SET TRANSACTION ISOLATION LEVEL <isolation_level>; START TRANSACTION 完全相同
SET TRANSACTION ISOLATION LEVEL READ COMMITTEDSTART TRANSACTION;UPDATE accountsSET balance = balance - 1500WHERE id = 1;UPDATE accountsSET balance = balance + 1500WHERE id = 2;/* Commit the data and start a new transaction that will take into account the committed data and isolation level from the last transaction */COMMIT AND CHAIN;UPDATE accountsSET balance = balance - 1000WHERE id = 2;UPDATE accountsSET balance = balance + 1000WHERE id = 3;COMMIT;
事务链有助于创建多个事务,而无需每次都明确设置事务级别或修改会话或全局默认值。
结论
事务提供了一些有用的功能,可以帮助您的数据保持连贯状态。但是,各种隔离级别都有一些您应该记住的权衡。确定适合您用例的适当保护级别可能需要一些探索和思考。如果事务运行时间很长,这一点尤其重要,因为数据库在提交发生之前可能会发生显著变化,这可能导致回滚和更多手动工作。
尽管事务有其缺点,但它们在许多场景中仍然很有用,因为它们对关系数据库应提供的 ACID 保证做出了重大贡献。了解何时使用它们,哪种隔离级别有意义,以及如何避免自动回滚是值得投入的知识。
