简介
事务是一种机制,它将多个语句封装到单个操作中,供数据库处理。数据库不是单独输入语句,而是能够将命令组解释为有凝聚力的单元并对其进行操作。这有助于确保数据集在许多密切相关的语句过程中的一致性。
在本指南中,我们将首先讨论什么是事务以及为什么它们是有益的。之后,我们将了解 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 会自动回滚事务。如果选择的隔离级别不允许,则当发生序列化错误时,它也会回滚事务。
要手动回滚在当前事务期间给出的语句,您可以使用 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;
在这里,我们能够从我们犯的错误中恢复,而不会丢失我们在事务中到目前为止所做的所有工作。回滚后,我们使用正确的语句继续按计划进行事务。
设置事务的隔离级别
要为事务设置所需的隔离级别,您可以将 ISOLATION LEVEL
子句添加到您的 START TRANSACTION
或 BEGIN
命令中。基本语法如下所示
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 的事务指南来了解更多信息。