2024 年 9 月 26 日: PostgreSQL 17 已发布!
支持的版本: 当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本: 开发中
不再支持的版本: 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2 / 7.1

13.2. 事务隔离 #

SQL标准定义了四个事务隔离级别。最严格的事务为可序列化,该事务在段落中由标准定义,内容为:一组可序列化事务的任何并发执行都保证产生与按特定顺序一次执行它们相同的效果。其他三个级别按照现象进行定义,由并发事务之间的交互产生,这种现象不能出现在每个级别。标准指出,由于可序列化的定义,这些现象都不会出现在该级别。(这点不足为奇——如果事务的效果必须与一次执行一项事务的效果保持一致,那么您如何看出由交互造成的任何现象呢?)

在不同级别被禁止的现象有

读取未提交的数据

事务读取由一个并发未提交事务写入的数据。

不可重复读

事务再次读取它先前已读取的数据,发现这些数据已被另一个事务(自最初读取后提交)修改。

幻读

事务重新执行查询,该查询返回满足搜索条件的一组行,并发现满足该条件的行集已因另一个最近提交的事务而改变。

序列化异常

顺利提交一组事务的结果与一次执行这些事务的所有可能顺序不一致。

SQL 标准及 PostgreSQL 实现的事务隔离级别在 表 13.1 中予以说明。

表 13.1。事务隔离级别

隔离级别 读取未提交的数据 不可重复读 幻读 序列化异常
读取未提交的内容 允许,但 PG 中不允许 可能 可能 可能
读取已提交的内容 不可能 可能 可能 可能
可重复读 不可能 不可能 允许,但 PG 中不允许 可能
可序列化 不可能 不可能 不可能 不可能

PostgreSQL 中,您可以请求任何四个标准事务隔离级别,但内部只实现三个不同的隔离级别,即 PostgreSQL 的读取未提交模式的行为就像读取已提交的内容。这是因为这是将标准隔离级别映射到 PostgreSQL 的多版本并发控制架构的唯一明智方式。

此表还显示 PostgreSQL 的可重复读实现不允许幻读。这在 SQL 标准中是可接受的,因为标准指定了在某些隔离级别下不能出现的异常;更高的保证是可接受的。可用隔离级别的行为在以下小节中详述。

若要设置事务的交易隔离级别,请使用命令 SET TRANSACTION

重要

某些 PostgreSQL 数据类型和函数对事务行为有特殊规则。特别是,对序列所作的变更(以及使用 serial 声明的列的计数器)会立即对所有其他事务可见,并且如果进行变更的事务中止,这些变更不会回滚。请参见 第 9.17 节第 8.1.4 节

13.2.1. 已提交读隔离级别 #

已提交读PostgreSQL 中的默认隔离级别。当事务使用此隔离级别时,SELECT 查询(不带 FOR UPDATE/SHARE 子句)只看到在查询开始之前已提交的数据;它不会看到未提交的数据,也不会看到查询执行期间由并发事务提交的变更。实际上,SELECT 查询看到的是在查询开始运行时数据库的一个快照。但是,SELECT 确实会看到其自身事务中以前执行的更新的影响,即使它们尚未提交。还要注意,即使在单一事务中,两个连续的 SELECT 命令也会看到不同的数据,前提是其他事务在第一个 SELECT 开始之后和第二个 SELECT 开始之前提交变更。

UPDATEDELETESELECT FOR UPDATESELECT FOR SHARE 命令在搜索目标行方面的行为与 SELECT 相同:它们只会找到截至命令开始时间已提交的目标行。但是,这样的目标行在被找到时可能已被另一个并发事务更新(或删除或锁定)。在这种情况下,更新器将等待第一个更新事务提交或回滚(如果它仍在进行中)。如果第一个更新器回滚,则取消其影响,并且第二个更新器可以继续更新最初找到的行。如果第一个更新器提交,如果第一个更新器删除了该行,则第二个更新器将忽略该行,否则它将尝试将其操作应用于该行的更新版本。该命令的搜索条件(WHERE 子句)将重新评估,以查看该行的更新版本是否仍然匹配搜索条件。如果是,则第二个更新器将使用该行的更新版本继续执行其操作。对于 SELECT FOR UPDATESELECT FOR SHARE,这意味着已更新版本的行将被锁定并且返回给客户端。

带有 ON CONFLICT DO UPDATE 子句的 INSERT 行为类似。在可重复读模式下,每个待插入的行都将插入或更新。除非有无关的错误,否则保证有这两种结果之一。如果冲突起源于其效果尚未对 INSERT 可见的其他事务中,则 UPDATE 子句将影响该行,即使可能该行的 没有 版本对该命令按照惯例可见。

带有 ON CONFLICT DO NOTHING 子句的 INSERT 可能由于另一个事务(其效果对 INSERT 快照不可见)的结果,导致一行的插入不会处理。同样,这仅在可重复读模式中出现。

MERGE 允许用户指定 INSERTUPDATEDELETE 子命令的各种组合。带有 INSERTUPDATE 子命令的 MERGE 命令看起来类似于带有 ON CONFLICT DO UPDATE 子句的 INSERT,但不保证 INSERTUPDATE 会发生。如果 MERGE 尝试 UPDATEDELETE,且行并发更新,但对于当前目标和当前源元组,联接条件仍然传递,则 MERGE 将表现得与 UPDATEDELETE 命令相同,并对该行的更新版本执行其操作。然而,因为 MERGE 可以指定若干操作并且它们可以是条件性的,所以对每个操作的条件都在行的更新版本上进行重新评估,从第一个操作开始,即使最初匹配的操作出现在操作列表中更后面的位置。另一方面,如果行的并发更新导致联接条件失败,则 MERGE 接下来将评估该命令的 NOT MATCHED BY SOURCENOT MATCHED [BY TARGET] 操作,并执行第一个成功的操作。如果行被并发删除,则 MERGE 将评估该命令的 NOT MATCHED [BY TARGET] 操作,并执行第一个成功的操作。如果 MERGE 尝试 INSERT,并且存在唯一索引,且同时插入的重复行违反了唯一性,则引发唯一性冲突错误;MERGE 不会通过重新启动 MATCHED 条件的评估来避免此类错误。

由于上述规则,一条正在更新的命令有可能看到一个不一致的快照:它可以看到同时在这条正在尝试更新的命令中对相同行的并发更新命令的影响,但是看不到这些命令对数据库中其他行的影响。这一行为使得 Read Committed 模式不适用于涉及复杂搜索条件的命令;但它适用于更简单的用例。比如,使用此类事务更新银行余额

BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;

如果这两个事务同时尝试变更帐号 12345 的余额,我们显然希望第二个事务从该帐号的行的更新版本开始。因为每个命令只影响一行预定的行,因此,让该命令看到该行的更新版本不会造成任何麻烦的不一致性。

更复杂的用法可能会在 Read Committed 模式中产生不良后果。比如,假定一条 DELETE 命令正在对数据进行操作,而另一条命令正在对其约束性标准进行添加和删除,例如,假设 website 是一个两行表,其中 website.hits 的值分别为 910

BEGIN;
UPDATE website SET hits = hits + 1;
-- run from another session:  DELETE FROM website WHERE hits = 10;
COMMIT;

虽然在 UPDATE 之前和之后都有一个 website.hits = 10 行,但 DELETE 不会产生任何效果。这是因为跳过了更新前的行值 9,并且当 UPDATE 完成且 DELETE 获得锁时,新的行值不再是 10,而是 11,不再与该标准匹配。

由于 Read Committed 模式会使用包含已提交至该具体时间的全部事务的新快照对每条命令进行初始化,因此,即使在上述情况下,同一事务中的后续命令都将看到提交并发事务的影响。上面所要讨论的是一条 单个 命令是否能够看到数据库的绝对一致视图。

Read Committed 模式所提供的部分事务隔离对于很多应用程序来说已经足够了,并且这种模式快速且易于使用;但它并不是对于所有用例来说都足够。那些执行复杂查询和更新的应用程序可能需要比 Read Committed 模式提供的更加严格一致的数据库视图。

13.2.2. 可重复读隔离级别 #

可重复读隔离级别只看到在事务开始前提交的数据;它绝不会看到未提交的数据,也不会看到在事务执行期间它由并发事务提交的变更。(但是,每个查询都会看到它自身在事务(虽然尚未提交)中执行的之前更新的影响。)这比SQL此隔离级别的标准,并防止 表 13.1 中描述的所有现象,但序列化异常除外。如上所述,该内容明确通过标准允许,该标准只描述每个隔离级别必须提供的 最低限度的 保护措施。

此级别不同于 Read Committed,因为可重复读事务中的查询将看到 事务 中的第一个非事务控制语句开始的快照,而不是事务中当前语句开始的快照。因此,单个 事务内的连续 SELECT 命令会看到相同的数据,即它们看不到它们自己的事务开始后提交的其他事务所做的更改。

使用此级别的应用程序必须准备好由于序列化失败而重试事务。

UPDATEDELETEMERGESELECT FOR UPDATESELECT FOR SHARE 命令在搜索目标行方面与 SELECT 的行为相同:它们只会找到在事务开始时间提交的目标行。但是,这样的目标行可能在找到时已被另一个并发事务更新(或删除或锁定)。在这种情况下,可重复读事务将等待第一个更新事务进行提交或回滚(如果它仍在进行中)。如果第一个更新者回滚,那么它的效果将被否定,可重复读事务可以继续更新最初找到的行。但如果第一个更新者提交(并且实际更新或删除了该行,而不仅仅是锁定了该行),那么可重复读事务将回滚并显示以下消息:

ERROR:  could not serialize access due to concurrent update

因为可重复读事务无法修改或锁定可重复读事务开始后其他事务所更改的行。

当应用程序收到此错误消息时,它应当中止当前事务并从头开始重试整个事务。第二次,该事务将把先前提交的更改作为它的数据库初始视图的一部分,因此在新行版本的起始点为新事务的更新使用新版本没有任何逻辑冲突。

注意,只有更新事务可能需要重试;只读事务永远不会产生序列化冲突。

可重复读模式能提供有力的保证,即每个事务都能看到数据库一个完全稳定的视图。然而,该视图并不一定会始终同级别并发事务的某个串行(逐个)执行保持一致。例如,即使在此级别一个只读事务可能会看到一个控制记录被更新(表示某个批处理已经完成),但会看到逻辑上是该批处理一部分的某个详细记录,因为它读取了该控制记录更早的版本。尝试在该隔离级别上运行事务来执行业务规则很可能无法正常工作,除非谨慎使用显式锁来阻止冲突事务。

可重复读隔离级别使用学术数据库文献和一些其他数据库产品中称为Snapshot Isolation的技术实现。当与采用传统锁技术来降低并发性的系统进行比较时,可能会观察到行为和性能方面的差异。一些其他系统甚至可能将可重复读和 Snapshot Isolation 作为行为不同的特有隔离级别提供。直到 SQL 标准开发完成后,才由数据库研究人员对区分这两种技术的允许现象进行了形式化,这超出了本手册的范围。若要进行全面处理,请参阅[berenson95]

注意

低于PostgreSQL 9.1 版本中,对 Serializable 事务隔离级别的请求可按这里描述确切地提供相同行为。为保留传统的 Serializable 行为,现在应请求可重复读。

13.2.3. Serializable 隔离级别#

Serializable 隔离级别提供最严格的事务隔离。该级别仿真所有已提交事务的串行事务执行;就像事务逐个(按顺序)执行而不是并发执行一样。然而,与可重复读级别类似,使用该级别的应用程序必须准备好处理由于序列化失败导致的事务重试。事实上,该隔离级别的作用与可重复读完全相同,除了它还会监视一些可能导致一组并发序列化事务的执行行为与所有可能的串行(逐个)执行方式不一致的条件。此监视不会引入任何超出可重复读现有的阻塞,但对于该监视存在一定开销,而且检测可能会导致序列化异常的条件将触发序列化失败

举个例子,考虑一个表mytab,最初包含以下内容

 class | value
-------+-------
     1 |    10
     1 |    20
     2 |   100
     2 |   200

假设可序列化事务 A 计算

SELECT SUM(value) FROM mytab WHERE class = 1;

然后将结果(30)插入到新行中作为 valueclass = 2。同时,可序列化的事务 B 计算

SELECT SUM(value) FROM mytab WHERE class = 2;

并获取结果 300,将结果插入到新行中,class = 1。然后两个事务都尝试提交。如果任一事务在可重复读取隔离级别上运行,则都将允许提交;但由于没有与结果一致的串行执行顺序,使用可序列化的事务将允许一个事务提交,并使用以下消息将另一个事务回滚

ERROR:  could not serialize access due to read/write dependencies among transactions

这是因为,如果 A 在 B 之前执行,则 B 将计算出总和 330 而不是 300,同样,另一个顺序将导致 A 计算出不同的总和。

当依赖 Serializable 事务来防止异常发生时,重要的是,从永久用户表中读取的任何数据在读取到它的事务成功提交之前都不能被视为有效。对于只读事务也是如此,除非在 可推迟 只读事务内读取的数据在被读取后立即被认为有效,因为这种事务在开始读取任何数据之前会一直等待,直到它可以获取一个保证没有此类问题的快照。在所有其他情况下,应用程序不得依赖在之后中止的事务期间所读取的结果;相反,它们应该重新尝试该事务,直到它成功。

为保证真正的可序列化,PostgreSQL 使用 谓词锁定,这意味着它保持锁定,以便能够确定,当一个写入对来自并发事务的先前读取的结果产生影响时,它(假设写入首先运行)是否运行。在 PostgreSQL 中,这些锁定不会造成任何阻塞,因此 不能 在引起死锁方面发挥任何作用。它们用于识别和标记并发可序列化事务之间的依赖关系,在某些组合中,会导致序列化异常。相比之下,希望确保数据一致性的已提交读或可重复读事务可能需要对整个表进行锁定,这可能会阻止其他用户试图使用该表,或者它可以使用 SELECT FOR UPDATESELECT FOR SHARE,这些不仅可以阻止其他事务,而且会导致磁盘访问。

PostgreSQL 及大多数其他数据库系统中的谓词锁基于事务实际访问的数据。这些内容将显示在 pg_locks 系统视图中,modeSIReadLock。查询执行期间获取的特定锁将取决于查询使用的计划,在事务过程中,多个细粒度锁(例如元组锁)可能会组合成较少的粗粒度锁(例如页面锁),以防止跟踪锁定所用内存耗尽。READ ONLY 事务如果检测到不会再发生任何冲突从而导致序列化异常,则能够在完成之前释放其 SIRead 锁。事实上,READ ONLY 事务通常能够在启动时确认该事实,并且避免采取任何谓词锁。如果您明确请求 SERIALIZABLE READ ONLY DEFERRABLE 事务,它将一直阻止,直至可以确认此事实。(这是可串行化事务阻止而可重复读取事务不会阻止的唯一情况。)另一方面,SIRead 锁通常需要在事务提交后保留,直至重叠的读写事务完成。

持续使用可串行化事务可以简化开发。保证任何成功的已提交并发可串行化事务集都与依次运行它们时效果相同,这意味着如果您能够证明单独运行已编写的单一事务时将执行正确操作,那么您可以确信它将在任何可串行化事务混合中执行正确操作,即使没有任何有关其他事务可能执行什么操作的信息,否则它将不会成功提交。重要的是,使用这种技术的环境需要一种处理序列化故障(总是使用 SQLSTATE 值“40001”返回)的通用方法,因为很难准确预测哪些事务可能导致读/写依赖关系并且需要回滚以防止序列化异常。监控读/写依赖关系有代价,就像终止带有序列化故障的事务也有代价一样,但与使用显式锁以及 SELECT FOR UPDATESELECT FOR SHARE 所涉及的代价和阻止相比,可串行化事务是某些环境的最佳性能选择。

虽然PostgreSQL的 Serializable 事务隔离级别仅允许并发事务在可以证明存在能产生相同效果的串行执行顺序条件下提交,但它并不能始终防止在真正的串行执行中不会发生的错误发生。特别是,即使在明确检查了插入之前键并不存在的情况下,也可能会看到由重叠 Serializable 事务引起的冲突导致唯一约束违规。可以通过确保所有插入潜在冲突键的 Serializable 事务明确检查它们是否首先可以这么做来避免这种情况。例如,假设一个应用程序向用户询问新键,然后通过尝试先选择该键检查该键是否已经存在,或通过选择现有最大键并添加 1 来生成新键。如果一些 Serializable 事务直接插入新键,而没有遵循此协议,那么即使在并发事务的串行执行中不会发生此情况时,也会报告唯一约束违规。

为了在依赖 Serializable 事务进行并发控制时获得最佳性能,应该考虑以下问题

  • 尽可能将事务声明为READ ONLY

  • 控制活动连接数,在需要时使用连接池。这始终是一个重要的性能考虑因素,但在使用 Serializable 事务的繁忙系统中它可能尤其重要。

  • 不要向单个事务中放入多于出于完整性目的所需的。

  • 不要让连接长时间处于未决态闲置于事务中。配置参数idle_in_transaction_session_timeout可以用于自动断开长时间处于未决态的会话。

  • 在 Serializable 事务自动提供的保护措施下不再需要时,消除显式锁、SELECT FOR UPDATESELECT FOR SHARE

  • 当系统由于谓词锁表中内存不足而被迫将多个个页面级谓词锁合并成单个关系级谓词锁时,可能会增加序列化失败的速率。您可以通过增加max_pred_locks_per_transactionmax_pred_locks_per_relation和/或max_pred_locks_per_page来避免这种情况。

  • 顺序扫描总是需要关系级谓词锁。这会导致序列化失败的速率增加。通过降低random_page_cost和/或增加cpu_tuple_cost,鼓励使用索引扫描可能会有所帮助。务必综合权衡事务回滚和重新启动的任何减少与查询执行时间的任何总体变化。

可序列化隔离级别使用一种在学术数据库文献中被称为可序列化快照隔离的技术实现,该技术通过添加对序列化异常的检查来构建快照隔离。与那些使用传统锁定技术的其他系统相比,可能会观察到一些行为和性能方面的差异。有关详细信息,请参见 [ports12]

提交更正

如果您在文档中看到任何内容不正确、与您使用特定功能的体验不符或需要进一步澄清,请使用 此表单 报告文档问题。