13.2. 事务隔离

SQL 标准定义了四个级别的事务隔离。最严格的是可序列化的,它是由标准在一段中定义的,该段说,保证并发执行一组可序列化事务的效果与按一定 Sequences 一次运行它们的效果相同。其他三个级别是根据并发事务之间的交互作用产生的现象定义的,该现象不能在每个级别上发生。该标准指出,由于可序列化的定义,在该级别上所有这些现象都是不可能的。 (这不足为奇-如果 Transaction 的效果必须与一次运行一致,那么您如何看待由交互引起的任何现象?)

在各个级别上禁止的现象是:

  • 脏读

    • 事务读取并发的未提交事务写入的数据。
  • 不可重复读

    • 一个事务重新读取它先前读取的数据,并发现该数据已被另一个事务修改(自首次读取以来提交的数据)。
  • 幻像阅读

    • 事务重新执行查询,返回满足搜索条件的一组行,并发现由于另一项最近提交的事务,满足该条件的一组行已更改。
  • 序列化异常

    • 成功提交一组事务的结果与一次运行这些事务的所有可能的 Sequences 不一致。

Table 13.1中描述了 SQL 标准和 PostgreSQL 实现的事务隔离级别。

表 13.1. Transaction 隔离级别

Isolation LevelDirty ReadNonrepeatable ReadPhantom ReadSerialization Anomaly
Read uncommitted允许,但不允许在 PG 中PossiblePossiblePossible
Read committedNot possiblePossiblePossiblePossible
Repeatable readNot possibleNot possible允许,但不允许在 PG 中Possible
SerializableNot possibleNot possibleNot possibleNot possible

在 PostgreSQL 中,您可以请求四个标准事务隔离级别中的任何一个,但内部仅实现三个不同的隔离级别,即 PostgreSQL 的 Read Uncommitted 模式的行为类似于 Read Committed。这是因为这是将标准隔离级别 Map 到 PostgreSQL 的多版本并发控制体系结构的唯一明智的方法。

该表还显示 PostgreSQL 的 Repeatable Read 实现不允许幻像读取。 SQL 标准允许更严格的行为:四个隔离级别仅定义了一定不能发生的现象,而不是必须发生的现象。以下各小节详细介绍了可用隔离级别的行为。

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

Important

某些 PostgreSQL 数据类型和函数具有关于事务行为的特殊规则。特别是,对序列所做的更改(以及因此使用serial声明的列的计数器)立即对所有其他事务可见,并且如果进行更改的事务中止,则不会回滚。参见Section 9.16Section 8.1.4

13 .2.1. 读取提交的隔离级别

  • Read Committed *是 PostgreSQL 中的默认隔离级别。当事务使用此隔离级别时,SELECT查询(无FOR UPDATE/SHARE子句)仅查看在查询开始之前提交的数据;它永远不会看到并发事务执行查询期间未提交的数据或所做的更改。实际上,SELECT查询会在查询开始运行时看到该数据库的快照。但是,SELECT确实会看到在其自己的事务中执行的先前更新的效果,即使尚未提交也是如此。还要注意,即使其他事务在第一个SELECT启动之后和第二个SELECT启动之前提交更改,即使它们在单个事务中,两个连续的SELECT命令也可以看到不同的数据。

UPDATEDELETESELECT FOR UPDATESELECT FOR SHARE命令在搜索目标行方面的行为与SELECT相同:它们将仅查找从命令开始时提交的目标行。但是,这样的目标行在被发现时可能已经被另一个并发事务更新(或删除或锁定)。在这种情况下,可能的更新程序将 await 第一个更新事务提交或回滚(如果仍在进行中)。如果第一个更新程序回滚,则其作用将被抵消,第二个更新程序可以 continue 更新最初找到的行。如果第一个更新程序提交,则第二个更新程序将忽略该行(如果第一个更新程序删除了该行),否则它将尝试将其操作应用于该行的更新版本。重新评估该命令(WHERE子句)的搜索条件,以查看该行的更新版本是否仍与搜索条件匹配。如果是这样,第二个更新程序将使用该行的更新版本 continue 其操作。对于SELECT FOR UPDATESELECT FOR SHARE,这意味着锁定并返回给 Client 端的是该行的更新版本。

带有ON CONFLICT DO UPDATE子句的INSERT的行为类似。在“读已提交”模式下,建议插入的每一行都将插入或更新。除非存在不相关的错误,否则将保证这两个结果之一。如果冲突是由另一个事务引起的,该事务对其INSERT尚不可见,则UPDATE子句将影响该行,即使该命令的* no *版本通常对命令而言是可见的。

由于另一事务的结果对于INSERT快照不可见,带有ON CONFLICT DO NOTHING子句的INSERT可能无法 continue 插入行。同样,只有在“读已提交”模式下才是这种情况。

由于上述规则,更新命令可能会看到不一致的快照:它可以在尝试更新的同一行上看到并发更新命令的效果,但看不到其他行上这些命令的效果在数据库中。此行为使“读已提交”模式不适用于涉及复杂搜索条件的命令;但是,这对于更简单的情况是正确的。例如,考虑使用以下 Transaction 更新银行余额:

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

如果两个这样的事务同时尝试更改帐户 12345 的余额,我们显然希望第二个事务以帐户行的更新版本开始。由于每个命令仅影响 sched 行,因此让其查看该行的更新版本不会造成任何麻烦的不一致。

在“读取已提交”模式下,更复杂的用法可能会产生不良结果。例如,考虑一个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,它不再符合条件。

由于“读取已提交”模式会以一个新快照开始每个命令,其中包括该瞬间之前已提交的所有事务,因此无论如何,同一事务中的后续命令都会看到已提交并发事务的效果。上面的问题是* single *命令是否看到数据库的绝对一致视图。

Read Committed 模式提供的部分事务隔离足以满足许多应用程序的需要,并且该模式使用起来快速简便。但是,这还不足以适用于所有情况。与读取提交模式相比,执行复杂查询和更新的应用程序可能需要更严格的数据库视图一致性。

13 .2.2. 重复读取隔离级别

“可重复读取”隔离级别仅查看在事务开始之前提交的数据。它永远不会看到并发事务在执行过程中未提交的数据或所做的更改。 (但是,查询确实会看到以前的更新在其自己的事务中执行的效果,即使它们尚未提交.)这比此隔离级别的 SQL 标准要求的保证要强,并且可以防止所有现象Table 13.1中描述的内容,但序列化异常除外。如上所述,这是标准特别允许的,该标准仅描述了每个隔离级别必须提供的“最小”保护。

此级别与“已提交读”不同,在可重复读事务中的查询看到快照是* transaction *中第一个非事务控制语句开始处的快照,而不是事务中当前语句开始处的快照。 。因此,单个事务中的连续SELECT命令将看到相同的数据,即,它们看不到其他事务在其自己的事务开始之后提交的更改。

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

在搜索目标行方面,UPDATEDELETESELECT FOR UPDATESELECT FOR SHARE命令的行为与SELECT相同:它们将仅查找在事务开始时已提交的目标行。但是,这样的目标行在被发现时可能已经被另一个并发事务更新(或删除或锁定)。在这种情况下,可重复读事务将 await 第一个更新事务提交或回滚(如果仍在进行中)。如果第一个更新程序回滚,则其效果将被否定,并且可重复读取事务可以 continue 更新原始找到的行。但是,如果第一个更新程序提交(并实际上更新或删除了该行,而不仅仅是锁定了该行),则可重复读事务将与消息一起回滚

ERROR:  could not serialize access due to concurrent update

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

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

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

可重复读取模式提供了严格的保证,即每个事务都能看到数据库的完全稳定的视图。但是,此视图不一定总是与同一级别的并发事务的某些串行(一次一个)执行一致。例如,即使是该级别的只读事务,也可能会看到更新了控制记录以表明批次已完成,但没有看到逻辑上属于该批次的详细记录之一,因为它读取了该批次的较早版本。控制记录。如果不谨慎使用显式锁来阻止冲突的事务,则试图以这种隔离级别运行的事务来强制执行业务规则的尝试不太可能正确执行。

Note

在 PostgreSQL 9.1 版之前,对 Serializable 事务隔离级别的请求提供了与此处所述完全相同的行为。为了保留传统的 Serializable 行为,现在应该请求重复读取。

13 .2.3. 可序列化的隔离级别

可序列化的隔离级别提供最严格的事务隔离。该级别模拟所有已提交事务的串行事务执行;就像事务是依次而不是并发执行的。但是,与可重复读取级别一样,由于序列化失败,使用该级别的应用程序必须准备重试事务。实际上,此隔离级别的工作原理与“可重复读取”完全相同,不同之处在于它监视的条件可能会使并发可序列化事务的并发执行方式与那些事务的所有可能的串行(一次执行)执行方式不一致。此监视不会引入任何超出可重复读取中的阻塞的阻塞,但是监视会有一些开销,并且可能导致“串行化异常” *的条件的检测将触发“串行化失败”。

例如,考虑一个表mytab,该表最初包含:

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

假设可序列化事务 A 计算:

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

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

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

并获得结果 300,并将其插入带有class = 1的新行中。然后,两个事务都尝试提交。如果两个事务都在“可重复读取”隔离级别上运行,则两个事务都可以提交;但是由于没有与结果一致的串行执行 Sequences,因此使用 Serializable 事务将允许提交一个事务,并使用此消息回退另一个事务:

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

这是因为,如果 A 已在 B 之前执行,则 B 将计算总和 330,而不是 300,并且类似地,其他 Sequences 将导致由 A 计算出不同的总和。

当依靠 Serializable 事务来防止异常时,从永久用户表中读取的任何数据都必须被视为有效,直到读取该事务的事务已成功提交,这一点很重要。即使对于只读事务也是如此,只是众所周知,在持久性只读事务中读取的数据在读取后就立即有效,因为这样的事务一直等到它可以获取保证是免费的快照之后从此类问题开始读取任何数据之前。在所有其他情况下,应用程序都不能依赖于在随后中止的事务期间读取的结果。相反,他们应该重试事务,直到成功为止。

为了保证 true 的可序列化性,PostgreSQL 使用* predicate Lock ,这意味着它保留了锁,这使它可以确定写操作如果对第一次从并发事务中读取的结果产生了影响(如果它先运行)。在 PostgreSQL 中,这些锁不会引起任何阻塞,因此不会在死锁中起任何作用。它们用于识别并标记并发可序列化事务之间的依赖性,这在某些组合中可能导致序列化异常。相反,想要确保数据一致性的“读已提交”或“可重复读”事务可能需要对整个表进行锁定,这可能会阻止其他试图使用该表的用户,或者它可能会使用SELECT FOR UPDATESELECT FOR SHARE,这不仅可以阻止其他事务,但导致磁盘访问。

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

一致使用 Serializable 事务可以简化开发。保证任何成功提交的并发可序列化事务集将具有与一次运行相同的效果,这意味着,如果您可以证明单个事务(如编写的那样)在单独运行时将做正确的事情,可以确信,即使没有任何有关那些其他事务可能做的信息,也不会成功提交,它可以在任何可序列化事务混合中做正确的事情。使用这种技术的环境必须具有通用的方式来处理序列化失败(该错误总是以 SQLSTATE 值“ 40001”返回),这一点很重要,因为很难准确地预测哪些事务可能会导致读/写。依赖项,需要回滚以防止序列化异常。监视读/写依赖关系以及重新启动因序列化失败而终止的事务都需要付出一定的代价,但要与使用显式锁和SELECT FOR UPDATESELECT FOR SHARE所涉及的成本和阻塞进行平衡,可序列化的事务是最佳性能选择对于某些环境。

尽管 PostgreSQL 的 Serializable 事务隔离级别仅允许并发事务提交,只要它可以证明存在可以产生相同效果的串行执行 Sequences,但它并不能始终防止引发 true 的串行执行中不会发生的错误。特别是,即使在尝试插入密钥之前明确检查了密钥是否存在之后,也有可能看到由与重叠的 Serializable 事务冲突引起的唯一约束冲突。通过确保插入所有潜在冲突密钥的* all *可序列化事务明确检查它们是否可以首先这样做,可以避免这种情况。例如,假设有一个应用程序要求用户提供一个新密钥,然后先尝试将其选中以检查它是否不存在,或者通过选择现有的最大密钥并添加一个新密钥来生成一个新密钥。如果某些可序列化事务在不遵循此协议的情况下直接插入新密钥,则即使在并发事务的串行执行中无法发生唯一约束冲突,也可能会报告它们。

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

  • 尽可能将 Transaction 声明为READ ONLY

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

  • 为了完整性目的,不要在单个事务中投入过多。

  • 不要使连接悬而未决的闲置时间超过必要。配置参数idle_in_transaction_session_timeout可用于自动断开延迟的会话。

  • 消除由于可序列化事务自动提供的保护而不再需要的显式锁SELECT FOR UPDATESELECT FOR SHARE

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

  • Sequences 扫描将始终需要关系级别的谓词锁定。这会导致序列化失败率增加。通过减少random_page_cost和/或增加cpu_tuple_cost来鼓励使用索引扫描可能会有所帮助。确保权衡事务回滚和重新启动的任何减少与查询执行时间的任何整体变化。