分享到

简介

事务是数据库将相关语句分组在一起,以便系统将其作为单个单元执行的一种方式。这些语句被视为一个单元,所有组成部分要么成功完成,要么恢复到其原始状态。这是一种保持一致性的方法,用于进行多个独立步骤才能实现单个目标的更改。

在本文中,我们将讨论什么是事务以及它们如何有用。然后我们将看到 MySQL 如何以各种方式使用事务来控制语句在数据库中的应用方式。

什么是事务?

事务 是一种将多个语句分组在一起并隔离以作为单个操作处理的方式。在将每个命令单独执行到服务器时,在事务中,命令被捆绑在一起,并在与其他请求不同的上下文中执行。

隔离 是事务的重要组成部分。在事务中,执行的语句只能影响事务本身的内部环境。从事务内部,语句可以修改数据,并且结果会立即显示。从外部,在事务提交之前不会进行任何更改,此时事务中的所有操作会立即变为可见。

这些功能通过提供原子性(事务中的操作要么全部提交要么全部回滚)和隔离(在事务之外,在提交之前不会进行任何更改,而在事务内部,每个语句都会立即产生影响)来帮助数据库实现ACID 兼容性。这些共同帮助数据库保持一致性(通过保证不会发生部分数据转换)。此外,事务中的更改在提交到非易失性存储器之前不会被返回为成功,这提供了持久性

为了实现这些目标,事务采用了多种不同的策略,不同的数据库系统使用不同的方法。MySQL 使用一个称为多版本并发控制 (MVCC) 的系统,它允许数据库使用数据快照执行这些操作。总之,这些系统构成了现代关系型数据库的基本构建块之一,使其能够以抗崩溃的方式安全地处理复杂数据。

一致性失败的类型

人们使用事务的一个原因是为了获得对其数据的完整性和处理数据的环境的某些保证。一致性可以通过多种方式被破坏,这会影响数据库尝试阻止它们的方式。

根据事务实现,一致性有四种主要方式会发生。您对可能出现这些情况的场景的容忍度将影响您在应用程序中使用事务的方式。

脏读

脏读 发生在事务中的语句能够读取其他正在进行的事务写入的数据时。这意味着即使事务的语句尚未提交,它们也可以被读取,从而影响其他事务。

这通常被认为是对一致性的严重破坏,因为事务没有被正确地从彼此隔离。可能永远不会提交到数据库的语句会影响其他事务的执行,修改它们的行为。

允许脏读的事务不能对最终数据的完整性做出任何合理的声明。

不可重复读

不可重复读 发生在事务之外的提交改变了事务中看到的数据时。如果在事务中,同一个数据被读取两次,但在每次实例中都检索到不同的值,您就可以识别出这种类型的错误。

与脏读一样,允许不可重复读的事务不会在事务之间提供完全的隔离。不同之处在于,对于不可重复读,影响事务的语句实际上已经提交到了事务之外。

幻读

一个幻读是一种特殊的不可重复读,发生在事务中,当第二次执行查询时,返回的行与第一次不同。

例如,如果事务中的查询在第一次执行时返回四行,但在第二次执行时返回五行,这就是幻读。幻读是由事务之外的提交导致的,这些提交改变了满足查询条件的行数。

序列化异常

序列化异常发生在多个事务并发提交时,导致结果与它们按顺序一个接一个提交时不同。这种情况可能发生在任何时候,只要一个事务允许两个提交发生,它们分别修改了同一个表或数据,而没有解决冲突。

事务隔离级别

事务不是“一刀切”的解决方案。不同的场景需要在性能和保护之间做出不同的权衡。幸运的是,MySQL 允许你指定所需的事务隔离类型。

大多数数据库系统提供的隔离级别包括以下几种:

读未提交

读未提交 是提供最少数据一致性和隔离保证的隔离级别。虽然使用 读未提交 的事务具有与事务相关的某些特性,例如能够一次提交多个语句或在发生错误时回滚语句,但它们确实允许许多情况发生,这些情况下可能会破坏一致性。

配置为 读未提交 隔离级别的事务允许:

  • 脏读
  • 不可重复读
  • 幻读
  • 序列化异常

这种隔离级别通常不推荐,因为它几乎不提供有关数据一致性和隔离性的保证。这主要在需要将语句组合在一起以实现“全有或全无”的提交模型,但不需要任何完整性保证的情况下使用(这种情况很少见)。

读已提交

读已提交 是一个隔离级别,专门防止脏读。当事务使用 读已提交 一致性级别时,未提交的数据永远不会影响事务的内部上下文。这通过确保未提交的数据永远不会影响事务,从而提供基本的一致性级别。

尽管 读已提交读未提交 提供了更好的保护,但它并不能防止所有类型的不一致。这些问题仍然可能出现:

  • 不可重复读
  • 幻读
  • 序列化异常

可重复读

The 可重复读隔离级别在 读已提交 提供的保证基础上构建。它像以前一样避免脏读,但也防止不可重复读。

虽然 可重复读 可以防止不可重复读和脏读,但它仍然可能遭受这些隔离问题:

  • 幻读
  • 序列化异常

在大多数情况下,幻读也会被阻止,但有一些情况下它们仍然可能发生。在链接的例子中,事务中的 SELECT 查询没有返回任何结果,即使在另一个事务插入并提交一行之后也是如此。然而,发出更新新行的查询会导致查询返回数据,即使更新不应该知道另一个事务提交的行。然后 SELECT 查询返回数据。

对于 MySQL 的 InnoDB 引擎(大多数情况下使用的正常引擎),可重复读隔离方法是默认设置。

可串行化

The 可串行化隔离级别提供最高级别的隔离和一致性。它防止了 可重复读 级别所防止的所有情况,同时还消除了序列化异常的可能性。

可串行化隔离保证并发事务的提交方式就好像它们是按顺序一个接一个执行的。如果发生可能导致序列化异常的情况,其中一个事务将发生序列化失败,而不是给数据集引入不一致性。

定义事务

现在我们已经介绍了 MySQL 在事务中使用的不同隔离级别,让我们演示如何定义事务。

在 MySQL 中,默认情况下,每个语句明确标记的事务之外实际上是在自己的单个语句事务中执行的。要显式启动事务块,可以使用 START TRANSACTIONBEGIN 命令。 START TRANSACTION 表单可以接受 BEGIN 表单不能接受的修饰符,因此 MySQL 建议你使用 START TRANSACTION。要提交事务,请发出 COMMIT 命令。

因此,事务的基本语法如下所示:

START TRANSACTION;
statements
COMMIT;

作为一个更具体的例子,假设我们试图从一个账户转账 1000 美元到另一个账户。我们要确保这笔钱始终在两个账户中的一个,但永远不会同时出现在两个账户中。

我们可以将封装这种转账的两个语句包装在一个类似于这样的事务中:

START TRANSACTION;
UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;
COMMIT;

在这里,如果没有将 1000 美元存入 id = 2 的账户,就不会从 id = 1 的账户中取出 1000 美元。虽然这两个语句事务中按顺序执行,但它们将同时提交,因此同时在基础数据集上执行。

回滚事务

在事务中,要么所有语句都提交到数据库,要么都不提交。放弃事务中的语句和修改,而不是将它们应用到数据库,被称为“回滚”事务。

事务可以自动或手动回滚。如果事务中的某个语句导致错误,或者在其他场景中为了避免问题,MySQL 会自动回滚事务。

要手动回滚当前事务期间给出的语句,可以使用 ROLLBACK 命令。这将取消事务中的所有语句,本质上是将时钟倒回到事务的开始。

例如,假设我们使用之前使用的同一个银行账户示例,如果我们在发出 UPDATE 语句后发现,我们不小心转错了金额或使用了错误的账户,我们可以回滚这些更改,而不是提交它们。

START TRANSACTION;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1500
WHERE 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 TRANSACTIONBEGIN 命令首次调用时的状态。但是,如果我们只想还原事务中的一些语句,该怎么办?

虽然您在发出ROLLBACK命令时无法指定任意回滚位置,但您可以回滚到事务过程中设置的任何“保存点”。您可以使用SAVEPOINT命令提前在事务中标记位置,然后在需要回滚时引用这些特定位置。

这些保存点允许您创建中间回滚点。然后,您可以选择性地恢复您当前位置与保存点之间执行的任何语句,然后继续处理您的事务。

要指定保存点,请发出SAVEPOINT命令,后跟保存点的名称。

SAVEPOINT save_1;

要回滚到该保存点,请使用ROLLBACK TO命令。

ROLLBACK TO save_1;

让我们继续使用我们一直在使用的以帐户为中心的示例。

START TRANSACTION;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
/* Set a save point that we can return to */
SAVEPOINT save_1;
UPDATE accounts
SET balance = balance + 1500
WHERE 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 accounts
SET balance = balance + 1500
WHERE id = 4;
COMMIT;

在这里,我们能够从错误中恢复,而不会丢失到目前为止在事务中完成的所有工作。回滚后,我们将使用正确的语句继续按计划执行事务。

设置事务隔离级别

要设置您想要的事务隔离级别,可以使用SET TRANSACTION语句,其中包含ISOLATION LEVEL子句。SET TRANSACTION语句允许您修改事务的隔离级别以及读写权限。

默认情况下,SET TRANSACTION语句只影响启动的下一个事务的属性。它必须在START TRANSACTIONBEGIN语句之前给出。基本语法如下所示。

SET TRANSACTION ISOLATION LEVEL <isolation_level>;
START TRANSACTION;
statements
COMMIT;

<isolation_level>可以是以下任何一种(前面已详细描述):

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ(MySQL 的默认操作模式)
  • SERIALIZABLE

您还可以在发出命令时提供关键字GLOBALSESSION以影响不同的范围。

如果您键入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 COMMITTED
START TRANSACTION;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1500
WHERE 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 accounts
SET balance = balance - 1000
WHERE id = 2;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 3;
COMMIT;

链接事务有助于创建多个事务,而无需每次都显式设置事务级别,或修改会话或全局默认值。

结论

事务提供了一些有用的功能,可以帮助您保持数据的一致性。但是,各种隔离级别有一些权衡,您应该尝试牢记。确定您的用例的适当保护级别可能需要一些探索和思考。如果事务运行时间很长,情况尤其如此,因为数据库可能会在提交发生之前发生重大变化,这会导致回滚和更多的手动工作。

即使有其缺点,事务在许多场景中仍然很有帮助,因为它们代表了关系数据库应提供的 ACID 保证的重大贡献。了解何时使用它们、哪种隔离级别有意义以及如何避免自动回滚都是值得投资的知识。

关于作者
Justin Ellingwood

Justin Ellingwood

Justin 自 2013 年以来一直在撰写有关数据库、Linux、基础设施和开发者工具的文章。他目前与妻子和两只兔子住在柏林。他通常不必以第三人称写作,这对所有相关方来说都是一种解脱。