Skip to content

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。读取已提交隔离级别#

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

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

INSERT带有ON CONFLICT DO UPDATE子句的行为类似。在已提交读模式中,提议插入的每一行都将插入或更新。除非有无关错误,否则保证这两个结果之一。如果冲突源自另一个事务,其影响尚未对INSERT可见,UPDATE子句将影响该行,即使该行的版本通常对命令不可见。

INSERT带有ON CONFLICT DO NOTHING子句可能会由于另一个事务的结果而导致一行无法插入,而该事务的结果对INSERT快照不可见。同样,这仅在已提交读模式下才会发生。

MERGE允许用户指定INSERTUPDATEDELETE子命令的各种组合。同时具有INSERTUPDATE子命令的MERGE命令看起来类似于带有ON CONFLICT DO UPDATE子句的INSERT,但不能保证INSERTUPDATE会发生。如果MERGE尝试UPDATEDELETE,并且该行同时更新,但连接条件仍然适用于当前目标和当前源元组,则MERGE的行为将与UPDATEDELETE命令相同,并在该行的更新版本上执行其操作。但是,由于MERGE可以指定多个操作,并且它们可以有条件,因此即使最初匹配的操作出现在操作列表的后面,也会从第一个操作开始,在该行的更新版本上重新评估每个操作的条件。另一方面,如果行同时更新或删除,导致连接条件失败,则MERGE将接下来评估该条件的NOT MATCHED操作,并执行第一个成功的操作。如果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]

注意

在PostgreSQL9.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 计算出不同的总和。

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

为了保证真正的可序列化性,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的可串行化事务隔离级别只允许并发事务在可以证明存在产生相同效果的串行执行顺序时提交,但它并不总是能防止出现真正的串行执行中不会发生的错误。特别是,即使在明确检查密钥在尝试插入之前不存在后,也有可能看到由与可串行化事务重叠的冲突引起的唯一约束冲突。可以通过确保所有插入可能冲突密钥的可串行化事务首先明确检查它们是否可以执行此操作来避免这种情况。例如,假设一个应用程序向用户询问一个新密钥,然后通过尝试首先选择它来检查它是否已经存在,或者通过选择最大现有密钥并添加一个来生成一个新密钥。如果某些可串行化事务直接插入新密钥而不遵循此协议,即使在并发事务的串行执行中不会发生此类情况,也可能会报告唯一约束冲突。

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

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

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

  • 不要将超出完整性目的所需的内容放入单个事务中。

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

  • 由于可序列化事务自动提供的保护,不再需要时,请消除显式锁、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]