简介
事务是一种机制,它将多个语句封装到一个数据库处理的单个操作中。数据库不是逐个输入语句,而是能够将语句组解释为一个连贯的单元并对其进行操作。这有助于在许多密切相关的语句过程中确保数据集的一致性。
在本指南中,我们将首先讨论什么是事务以及为什么它们是有益的。之后,我们将看看 PostgreSQL 如何实现事务以及使用事务时可用的各种选项。
什么是事务?
事务 是将多个语句组合在一起并隔离起来,以便作为一个操作进行处理的方法。数据库不是在发送到服务器时单独执行每个命令,而是在事务中,命令被捆绑在一起并在与其他请求不同的上下文中执行。
隔离是事务的重要组成部分。在事务中,执行的语句只能影响事务本身的环境。从事务内部,语句可以修改数据,结果会立即显示。从外部,直到事务提交,才不会进行任何更改,此时事务中的所有操作将同时变得可见。
这些特性有助于数据库通过提供 原子性(事务中的操作要么全部提交,要么全部回滚)和 隔离性(在事务外部,直到提交,否则不会有任何更改,而在事务内部,语句会有影响)来实现 ACID 兼容性。这些特性共同帮助数据库维护 一致性(通过保证不会发生部分数据转换)。此外,直到更改提交到非易失性存储中,事务中的更改才会被视为成功,这提供了 持久性。
为了实现这些目标,事务采用了多种不同的策略,不同的数据库系统使用不同的方法。PostgreSQL 使用称为 多版本并发控制 (MVCC) 的系统,该系统允许数据库在不使用不必要的锁定的情况下使用数据快照执行这些操作。总而言之,这些系统构成了现代关系型数据库的基础基石之一,使它们能够以抗崩溃的方式安全地处理复杂数据。
一致性失败类型
人们使用事务的一个原因是为了获得关于其数据的一致性及其处理环境的某些保证。一致性可以通过多种方式被破坏,这会影响数据库如何尝试防止它们。
根据事务的实现,有四种主要的不一致性出现方式。您对这些场景可能出现的容忍度会影响您在应用程序中使用事务的方式。
脏读
脏读 发生在事务中的语句能够读取其他正在进行的事务写入的数据时。这意味着即使事务的语句尚未提交,但仍然可以读取它们,从而影响其他事务。
这通常被认为是严重违反了一致性,因为事务没有得到充分地彼此隔离。可能永远不会提交到数据库的语句会影响其他事务的执行,从而修改其行为。
允许脏读的事务不能对生成数据的的一致性做出任何合理的保证。
不可重复读
不可重复读 发生在事务外部的提交更改了事务内看到的数据时。如果在事务中两次读取相同的数据,但每次检索到的值不同,那么您可以识别出这种类型的问题。
与脏读一样,允许不可重复读的事务不会提供事务之间的完全隔离。区别在于,对于不可重复读,影响事务的语句实际上已在事务外部提交。
幻读
幻读(幻读)是一种特殊的不可重复读,发生在同一个事务中,第二次执行查询返回的结果与第一次不同。
例如,如果事务中的某个查询第一次执行返回四行,第二次执行返回五行,这就是幻读。幻读是由事务之外的提交操作更改了满足查询条件的行数引起的。
序列化异常
当多个并发提交的事务导致的结果与按顺序提交的结果不同时,就会发生序列化异常。这种情况发生在多个事务修改同一个表或数据,但没有解决冲突的情况下。
序列化异常是早期事务类型无法理解的一种特殊问题。因为早期事务的实现使用了锁,如果另一个事务正在读取或修改相同的数据,则当前事务无法继续执行。
事务隔离级别
事务并非“一刀切”的解决方案。不同的场景需要在性能和保护之间进行权衡。幸运的是,PostgreSQL 允许您指定所需的事务隔离类型。
大多数数据库系统提供的隔离级别包括以下几种:
读未提交
读未提交是隔离级别中对维护数据一致性和隔离性提供最少保证的级别。虽然使用read uncommitted
的事务具有与事务相关的某些功能,例如能够一次提交多个语句或在发生错误时回滚语句,但它们确实允许发生许多会破坏一致性的情况。
配置为read uncommitted
隔离级别的事务允许:
- 脏读
- 不可重复读
- 幻读
- 序列化异常
PostgreSQL 实际上并未实现这种隔离级别。虽然 PostgreSQL 识别隔离级别名称,但在内部,它实际上并不支持,而是会使用“读已提交”(如下所述)。
读已提交
读已提交是一种隔离级别,专门防止脏读。当事务使用read committed
一致性级别时,未提交的数据永远不会影响事务的内部上下文。这通过确保未提交数据永远不会影响事务来提供基本的一致性级别。
虽然read committed
比read uncommitted
提供了更高的保护,但它并不能防止所有类型的一致性问题。这些问题仍然可能出现
- 不可重复读
- 幻读
- 序列化异常
如果未指定其他隔离级别,PostgreSQL 将默认使用read committed
级别。
可重复读
可重复读隔离级别是在read committed
提供的保证基础上构建的。它与之前一样避免脏读,但同时也防止了不可重复读。
这意味着,在事务之外提交的更改永远不会影响事务内部读取的数据。在事务开始时执行的查询永远不会在事务结束时产生不同的结果,除非是事务内部的语句直接导致的结果。
虽然repeatable read
隔离级别的标准定义只要求防止脏读和不可重复读,但 PostgreSQL 在此级别也防止了幻读。这意味着,在事务之外提交的更改不会更改满足查询条件的行数。
由于事务中看到的数据状态可能偏离数据库中的最新数据,因此如果两个数据集无法协调,事务可能会在提交时失败。因此,此隔离级别的缺点之一是,如果在提交时发生序列化失败,您可能需要重试事务。
PostgreSQL 的repeatable read
隔离级别阻止了大多数类型的一致性问题,但序列化异常仍然可能发生。
可序列化
可序列化隔离级别提供最高级别的隔离和一致性。它阻止了repeatable read
级别阻止的所有场景,同时也消除了序列化异常的可能性。
可序列化隔离保证并发事务的提交顺序与按顺序执行时相同。如果发生可能引入序列化异常的场景,则其中一个事务将发生序列化失败,而不是对数据集引入不一致。
定义事务
现在我们已经介绍了 PostgreSQL 在事务中使用的不同隔离级别,让我们演示如何定义事务。
在 PostgreSQL 中,每个在显式标记的事务之外的语句实际上都在自己的单个语句事务中执行。要显式启动事务块,可以使用BEGIN
或START TRANSACTION
命令(它们是同义词)。要提交事务,请发出COMMIT
命令。
因此,事务的基本语法如下所示:
BEGIN;statementsCOMMIT;
作为一个更具体的例子,想象一下,我们正在尝试将 1000 美元从一个账户转到另一个账户。我们希望确保这笔钱始终在两个账户中的一个账户里,而不会同时出现在两个账户里。
我们可以将一起封装此转账的两个语句包装在一个类似于以下所示的事务中:
BEGIN;UPDATE accountsSET balance = balance - 1000WHERE id = 1;UPDATE accountsSET balance = balance + 1000WHERE id = 2;COMMIT;
在此,不会在没有将 1000 美元存入id = 2
的账户的情况下,从id = 1
的账户中取出 1000 美元。虽然这两个语句在事务内部按顺序执行,但它们将同时提交,因此在底层数据集中执行。
回滚事务
在事务中,所有语句要么全部提交到数据库,要么都不提交。放弃事务中执行的语句和修改,而不是将它们应用到数据库,被称为“回滚”事务。
事务可以自动或手动回滚。如果事务中的某个语句导致错误,PostgreSQL 会自动回滚事务。如果选定的隔离级别不允许序列化错误,PostgreSQL 也会回滚事务。
要手动回滚当前事务中执行的语句,可以使用ROLLBACK
命令。这将取消事务中的所有语句,本质上将时间倒回到事务开始时。
例如,假设我们使用之前使用的相同银行账户示例,如果我们在发出UPDATE
语句后发现错误地转账了错误的金额或使用了错误的账户,我们可以回滚更改,而不是提交它们:
BEGIN;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
命令将事务重置为首次调用BEGIN
或START TRANSACTION
命令时的状态。但是,如果我们只想撤消事务中的某些语句,该怎么办?
虽然在执行 ROLLBACK
命令时无法指定任意回滚点,但您可以回滚到事务中设置的任何“保存点”。您可以使用 SAVEPOINT
命令预先标记事务中的位置,并在需要回滚时引用这些特定位置。
这些保存点允许您创建一个中间回滚点。然后,您可以选择性地撤消从当前位置到保存点之间执行的任何语句,然后继续处理您的事务。
要指定保存点,请执行 SAVEPOINT
命令,后跟保存点的名称。
SAVEPOINT save_1;
要回滚到该保存点,请使用 ROLLBACK TO
命令。
ROLLBACK TO save_1;
让我们继续我们一直在使用的以帐户为中心的示例。
BEGIN;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;
在这里,我们能够从错误中恢复,而不会丢失迄今为止在事务中完成的所有工作。回滚后,我们将继续使用正确的语句按计划执行事务。
设置事务的隔离级别
要设置事务所需的隔离级别,可以在 START TRANSACTION
或 BEGIN
命令中添加 ISOLATION LEVEL
子句。基本语法如下所示。
BEGIN ISOLATION LEVEL <isolation_level>;statementsCOMMIT;
<isolation_level>
可以是以下任何一个(前面已详细描述)。
READ UNCOMMITTED
(将导致READ COMMITTED
,因为 PostgreSQL 中未实现此级别)。READ COMMITTED
REPEATABLE READ
SERIALIZABLE
SET TRANSACTION
命令也可以在事务启动后用于设置隔离级别。但是,只能在执行任何查询或数据修改命令之前使用 SET TRANSACTION
,因此它不提供更高的灵活性。
链接事务
如果有多个事务应按顺序执行,可以选择使用 COMMIT AND CHAIN
命令将它们链接在一起。
COMMIT AND CHAIN
命令通过提交其中的语句来完成当前事务。提交处理完成后,它会立即打开一个新事务。这允许您将另一组语句分组在一个事务中。
该语句的工作方式与执行 COMMIT; BEGIN
相同。
BEGIN;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 from the last transaction */COMMIT AND CHAIN;UPDATE accountsSET balance = balance - 1000WHERE id = 2;UPDATE accountsSET balance = balance + 1000WHERE id = 3;COMMIT;
链接事务在功能方面没有太多优势,但它可以帮助在自然边界处提交数据,同时继续专注于相同类型的操作。
结论
事务不是灵丹妙药。各种隔离级别带来了很多权衡,了解需要保护哪些类型的一致性需要思考和计划。对于长时间运行的事务来说尤其如此,因为底层数据可能会发生重大变化,与其他并发事务发生冲突的可能性也会增加。
尽管如此,事务机制提供了很大的灵活性和强大功能。它在很大程度上确保了即使在执行相互关联的并发操作时也能维护 ACID 保证。了解何时以及如何正确使用事务来执行复杂的安全操作非常宝贵。
如果您使用的是 JavaScript 或 TypeScript,可以使用 Prisma 管理您的 PostgreSQL 数据库。使用 事务 API 的任何操作都将使用 PostgreSQL 服务器的默认隔离级别。作为使用交互式事务的替代方法,Prisma 还通过 嵌套写入 和 批量操作 提供事务行为。您可以通过阅读 Prisma 的事务指南 来了解更多信息。