简介
事务是数据库将相关语句组合在一起,作为系统的一个独立单元执行的方式。这些语句作为一个单元处理,所有组成部分要么成功完成,要么回滚到原始状态。这是在需要多个独立步骤才能实现一个目标时,保持数据一致性的一种方式。
本文将讨论什么是事务以及它们的用处。然后我们将看到 MySQL 如何以各种方式使用事务来精确控制语句如何应用于数据库。
什么是事务?
事务是一种将多个语句分组并隔离起来,作为单个操作进行处理的方式。在事务中,命令不是在发送到服务器时单独执行,而是捆绑在一起并在与其他请求分离的上下文中执行。
隔离是事务的重要组成部分。在事务内部,执行的语句只能影响事务本身的环境。从事务内部看,语句可以修改数据,结果立即可见。从外部看,在事务提交之前,不会进行任何更改,此时事务内的所有操作将同时变得可见。
这些特性通过提供原子性(事务中的操作要么全部提交,要么全部回滚)和隔离性(在事务外部,直到提交前没有任何变化,而在内部,每个语句都有即时后果)来帮助数据库实现 ACID 合规性。这些共同帮助数据库保持一致性(通过保证不会发生部分数据转换)。此外,事务中的更改在提交到非易失性存储之前不会被认为是成功的,这提供了持久性。
为了实现这些目标,事务采用了多种不同的策略,不同的数据库系统使用不同的方法。MySQL 使用一种称为多版本并发控制 (MVCC) 的系统,它允许数据库使用数据快照执行这些操作。总而言之,这些系统构成了现代关系型数据库的基本构建块之一,使其能够以抗崩溃的方式安全地处理复杂数据。
一致性失败的类型
人们使用事务的原因之一是获得关于数据一致性及其处理环境的某些保证。一致性可以通过许多不同的方式被破坏,这影响了数据库如何尝试阻止它们。
根据事务实现的不同,可能出现四种主要的不一致性方式。您对这些场景可能出现的容忍度将影响您如何在应用程序中使用事务。
脏读
脏读发生在事务内的语句能够读取其他正在进行中的事务写入的数据时。这意味着即使事务的语句
这通常被认为是严重的一致性破坏,因为事务之间没有正确隔离。可能永远不会提交到数据库的语句会影响其他事务的执行,从而修改它们的行为。
允许脏读的事务无法对结果数据的一致性做出任何合理的声明。
不可重复读
不可重复读发生在事务外部的提交更改了事务内部可见的数据时。如果在事务内部,相同的数据被读取两次但每次检索到的值不同,则可以识别出这种类型的问题。
与脏读一样,允许不可重复读的事务不能提供事务之间的完全隔离。区别在于,对于不可重复读,影响事务的语句实际上已在事务外部提交。
幻读
幻读是一种特殊类型的不可重复读,它发生在查询返回的行在事务内第二次执行时不同。
例如,如果事务内的查询第一次执行时返回四行,而第二次执行时返回五行,这就是幻读。幻读是由事务外部的提交更改了满足查询条件的行数引起的。
序列化异常
序列化异常发生在多个并发提交的事务的结果与它们一个接一个提交的结果不同时。这可能发生在任何时候,当一个事务允许两个提交发生,每个都修改相同的表或数据而不解决冲突。
事务隔离级别
事务并非“一刀切”的解决方案。不同的场景需要在性能和保护之间进行不同的权衡。幸运的是,MySQL 允许您指定所需的事务隔离类型。
大多数数据库系统提供的隔离级别包括以下内容:
读未提交 (Read uncommitted)
读未提交 (Read uncommitted) 是提供最少数据一致性和隔离性保证的隔离级别。虽然使用 read uncommitted
的事务具有一些与事务相关的特性,例如能够一次性提交多个语句或在发生错误时回滚语句,但它们
配置了 read uncommitted
隔离级别的事务允许:
- 脏读
- 不可重复读
- 幻读
- 序列化异常
此隔离级别通常不推荐使用,因为它几乎不提供任何数据一致性和隔离性保证。这主要适用于您需要将语句分组以实现“全有或全无”的提交模型,但不需要任何完整性保证的情况(这种情况非常罕见)。
读已提交 (Read committed)
读已提交 (Read committed) 是一种专门防止脏读的隔离级别。当事务使用 read committed
一致性级别时,未提交的数据永远不会影响事务的内部上下文。这通过确保未提交的数据永远不会影响事务来提供基本的一致性级别。
尽管 read committed
提供比 read uncommitted
更大的保护,但它并不能防止所有类型的不一致。以下问题仍然可能出现:
- 不可重复读
- 幻读
- 序列化异常
可重复读 (Repeatable read)
可重复读 (repeatable read) 隔离级别建立在 read committed
提供的保证之上。它像以前一样避免脏读,同时也能防止不可重复读。
尽管 repeatable read
可以防止不可重复读和脏读,但它仍然可能受到以下隔离问题的影响:
- 幻读
- 序列化异常
在大多数情况下,幻读也会被阻止,但在某些情况下它们可能仍然发生。在链接的示例中,事务中的 SELECT
查询返回空结果,即使在另一事务插入并提交了一行之后。然而,执行一个将更新新行的查询会导致查询返回数据,即使更新不应该知道由另一事务提交的行。此后,SELECT
查询会返回数据。
对于 MySQL 的 InnoDB 引擎(大多数情况下的默认引擎),可重复读隔离方法是默认的。
串行化 (Serializable)
串行化 (Serializable) 隔离级别提供了最高级别的隔离和一致性。它阻止了 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 = 2
的账户,则不会从 id = 1
的账户中取出 1000 美元。虽然这两个语句在事务
回滚事务
在一个事务中,所有语句要么全部提交到数据库,要么全部不提交。放弃在事务中进行的语句和修改而不将其应用于数据库被称为“回滚”事务。
事务可以自动回滚或手动回滚。如果事务中的某个语句导致错误或在其他情况下为避免问题,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 UNCOMMITTED
READ COMMITTED
REPEATABLE 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 保证做出了重大贡献。了解何时使用它们,哪种隔离级别有意义,以及如何避免自动回滚是值得投入的知识。