Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

13.2. 事务隔离 #

13.2.1. 读提交隔离级别
13.2.2. 可重复读隔离级别
13.2.3. 可串行化隔离级别

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. Read Committed 隔离级别 #

Read CommittedPostgreSQL 中的默认隔离级别。当事务使用此隔离级别时,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 子句将影响该行,即使该行的版本通常对该命令不可见。p

带有 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条件的评估来避免此类错误。

由于上述规则,更新命令有可能会看到不一致的快照:它能够看到在它尝试更新的行上进行的并发更新命令的效果,但无法看到这些命令对数据库中其他行产生的效果。此行为使得已提交读模式不适合包含复杂搜索条件的命令;但是,它对于那些更简单的案例却是恰到好处。例如,考虑通过事务来更新银行余额,如下例所示

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

如果两个此类事务同时尝试更改账户12345的余额,那么我们显然希望第二个事务从账户行的已更新版本开始。由于每个命令只会影响一个预先确定的行,因此让它看到行的已更新版本不会产生任何麻烦的不一致。

在读提交模式下,更复杂的使用可能会产生不良结果。例如,假设 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,这不再符合条件。

因为读提交模式使用包含提交至该时刻前所有事务的新快照对每个命令进行启动,所以同个事务中的后续命令会在任何情况下都会看到提交的并发事务的影响。以上的一个争论点是 单个 命令是否能看到一个绝对一致的数据库视图。

读提交模式提供的部分事务隔离对于许多应用而言已足够,且该模式快速且使用简单;但是,它并非适用于所有情况。执行复杂查询和更新的应用可能需要比读提交模式所提供的更加严格一致的数据库视图。

13.2.2. 可重复读隔离级别 #

可重复读 隔离级别只看到在事务开始前已提交的数据;它既不会看到未提交的数据,也不会在事务执行期间看到由并发事务提交的更改。(但每次查询确实看得到在其自己的事务中执行的先前更新的影响,即便它们尚未提交。)这是一个比此隔离级别的SQL标准要求更强的担保,并防止了 表 13.1 中描述的所有现象,除了序列化的异常。如上所述,这是标准明确允许的,它只描述了每个隔离级别必须提供的最低保护。

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

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

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

ERROR:  could not serialize access due to concurrent update

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

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

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

可重复读模式严格地保证每个事务都会看到数据库一个完全稳定的视图。但是,此视图不一定总是与同一级别的并发事务的某个串行(一次)执行相一致。例如,即使此级别的只读事务也可能会看到控件记录已被更新,以显示批处理已经完成,但 不会 看到逻辑上属于该批处理的某个详细记录,因为它读取了控件记录的早期版本。如果没有仔细使用显式锁定来阻止冲突事务,则在该隔离级别运行的事务尝试强制执行业务规则很可能无法正确工作。

重复读取隔离级别使用一种技术实现,这种技术在学术数据库文献和某些其他数据库产品中称为快照隔离。与使用可降低并发性的传统锁定技术对系统进行比较时,可能会观察到行为和性能的差异。一些其他系统甚至可能提供重复读取和快照隔离作为具有不同行为的不同隔离级别。区分这两种技术的允许现象直到 SQL 标准开发出来之后才由数据库研究人员正式提出,并且超出了本手册的范围。要获得详尽的处理,请参见 [berenson95]

注意

PostgreSQL 9.1 版之前,对可串行化事务隔离级别的请求提供了这里描述的完全相同行为。为了保留传统的可串行化行为,现在应请求重复读取。

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 的新行。然后,这两个事务都尝试提交。如果任一事务在重复读取隔离级别中运行,则都允许它们提交;但由于没有与结果一致的串行执行顺序,因此使用可串行化事务将允许一个事务提交,并将另一个事务回滚并显示此消息

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

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

在依靠 Serializable 事务防止异常时,重要的是,从永久用户表读取的任何数据在读取它的事务成功提交后才会被认为有效。即使对只读事务而言也是如此,但读写可延期的只读事务中的数据在读取后立即就被认为有效,因为此类事务等待直到它能获取一个快照,以确保此类事务在开始读取任何数据之前不受此类问题的影响。在所有其他情况下,应用程序都不得依赖在之后中止的事务中读取的结果;相反,应用程序应重试该事务,直至它成功。

为了保证真正的可串行性,PostgreSQL 使用谓词锁定,这意味着它保持锁定,以便确定如果一个写入已首先运行,它会如何影响从并发事务之前读取的结果。PostgreSQL 中的这些锁定不会导致任何阻塞,因此不能导致死锁。它们用于识别和标记并发 Serializable 事务之间的依赖关系,这些依赖关系在某些组合中会导致串行化异常。相反,Read Committed 或 Repeatable Read 事务需要确保数据一致性,则可能需要获取整个表的锁定,该锁定可能会阻塞尝试使用该表的其他用户,或者它可能会使用 SELECT FOR UPDATESELECT FOR SHARE,这些不仅可以阻塞其他事务,还会导致磁盘访问。

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

持续使用可序列化事务可以简化开发。确保任何成功提交的并发可序列化事务集的效果与一次运行的效果相同意味着如果你可以证明按原样编写的单个事务在单独运行时可以正常运行,那么你可以确信它在任何可序列化事务组合中都可以正常运行,即使不知道其他事务可能会做什么,或者它不会成功提交。使用这种技术的环境必须有一种处理序列化失败(始终以 SQLSTATE 值“40001”返回)的通用方法,这一点很重要,因为很难准确预测哪些事务可能导致读/写相关性并需要回滚以防止序列化异常。监控读/写相关性是有成本的,终止于序列化失败的事务的重新开始也是有成本的,但与使用显式锁和 SELECT FOR UPDATESELECT FOR SHARE 涉及的成本和阻塞相比,可序列化事务是某些环境中的最佳性能选择。

虽然 PostgreSQL 的可串行化事务隔离级别仅允许多个事务在它可以证明存在一系列执行会产生相同效果的情况下提交,但它不会总是阻止引发错误,这些错误在真正的串行执行中不会发生。特别是,即使在明确检查插入之前密钥不存在的情况下,也可能看到冲突于并发可串行化事务而造成的唯一约束违例。通过确保 所有 插入潜在冲突密钥的可串行化事务在先明确检查它们是否能进行插入,可以避免这种情况。例如,想象一个应用程序向用户请求新密钥,然后通过先尝试选择新密钥来检查密钥是否已经存在,或者通过选择最大的现有密钥并添加一个来生成新密钥。如果一些可串行化事务不遵循此协议而直接插入新密钥,即使在串行执行并发事务的情况下它们也不会发生,但仍可能报告唯一约束违例。

当依靠可串行化事务进行并发控制时,要获得最佳性能,应考虑以下问题

  • 尽可能将事务声明为 只读事务

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

  • 不要为完整性目的将多余内容放入单个事务中。

  • 不要让连接悬置 处于事务空闲态 超过必要时间。配置参数 idle_in_transaction_session_timeout 可用于自动断开滞留会话。

  • 在不再需要可串行化事务自动提供的保护的情况下,消除显式锁、用于更新的 SELECT用于共享的 SELECT

  • 当系统因谓词锁表内存不足而被迫将多个页面级谓词锁合并为单个关系级谓词锁时,可能会增加串行化故障率。可以通过增加 max_pred_locks_per_transactionmax_pred_locks_per_relation 和/或 max_pred_locks_per_page 来避免这种情况。

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

可序列化隔离级别使用学术数据库文献中称为“可序列化快照隔离”的技术实现,该技术通过添加对序列化异常的检查来构建快照隔离。与使用传统锁定技术的其他系统进行比较时,可能会观察到一些行为和性能差异。请参阅[ports12]以获取详细信息。