该SQLSQL 标准定义了四种事务隔离级别。最严格的是串行化 (Serializable),它由标准中一段文字定义,该段文字说明:任何一组串行化事务的并发执行,其结果都保证与按某种顺序逐个运行这些事务的结果相同。其他三个级别则通过并发事务交互产生的现象来定义,这些现象在每个级别上都必须避免。标准指出,由于串行化的定义,在串行化级别上不可能出现这些现象。(这并不奇怪——如果事务的结果必须与逐个运行事务的结果一致,怎么可能看到由交互引起的任何现象?)
在不同级别上被禁止的现象是:
SQL 标准和 PostgreSQL 实现的事务隔离级别在表 13.1 中进行了描述。
表 13.1. 事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 串行化异常 |
---|---|---|---|---|
未提交读 | 允许,但在 PG 中不实现 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 允许,但在 PG 中不实现 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 | 不可能 |
在 PostgreSQL 中,您可以请求任何四种标准的事务隔离级别,但内部只实现了三种不同的隔离级别,即 PostgreSQL 的未提交读模式的行为类似于读已提交。这是因为这是将标准隔离级别映射到 PostgreSQL 的多版本并发控制 (MVCC) 体系结构的一种合理方式。
该表还显示,PostgreSQL 的可重复读实现不允许幻读。这在 SQL 标准下是可以接受的,因为标准规定了在特定隔离级别下 不得 发生的现象;更高的保证是可以接受的。可用隔离级别的行为在以下子节中详细介绍。
要设置事务的隔离级别,请使用 SET TRANSACTION 命令。
一些 PostgreSQL 的数据类型和函数在事务行为方面有特殊的规则。特别是,对序列(以及声明为 serial
的列的计数器)所做的更改会立即对所有其他事务可见,并且如果进行更改的事务被回滚,则这些更改不会被回滚。请参阅第 9.17 节和第 8.1.4 节。
读已提交 (Read Committed) 是 PostgreSQL 中的默认隔离级别。当一个事务使用此隔离级别时,SELECT
查询(不带 FOR UPDATE/SHARE
子句)只能看到查询开始前已提交的数据;它永远看不到并发事务在查询执行期间未提交的数据或已提交的更改。实际上,SELECT
查询看到的是查询开始运行那一刻数据库的快照。但是,SELECT
可以看到其自身事务中先前执行的更新的效果,即使这些更新尚未提交。另外请注意,在单个事务中,两个连续的 SELECT
命令可以看到不同的数据,如果其他事务在第一个 SELECT
开始之后、第二个 SELECT
开始之前提交了更改。
UPDATE
、DELETE
、SELECT FOR UPDATE
和 SELECT FOR SHARE
命令在搜索目标行方面与 SELECT
的行为相同:它们只会找到在命令开始时已提交的目标行。但是,当找到这样的目标行时,它可能已被另一个并发事务更新(或删除或锁定)。在这种情况下,即将进行更新的事务将等待第一个更新事务提交或回滚(如果它仍在进行中)。如果第一个更新程序回滚,则其效果将被撤销,第二个更新程序可以继续更新最初找到的行。如果第一个更新程序提交,那么如果第一个更新程序删除了该行,第二个更新程序将忽略该行,否则它将尝试对其更新后的版本应用其操作。命令的搜索条件(WHERE
子句)会被重新评估,以查看更新后的行版本是否仍然匹配搜索条件。如果匹配,第二个更新程序将使用该行更新版本继续执行其操作。对于 SELECT FOR UPDATE
和 SELECT FOR SHARE
,这意味着被锁定和返回给客户端的是行的更新版本。
带有 ON CONFLICT DO UPDATE
子句的 INSERT
的行为类似。在读已提交模式下,每个拟插入的行要么插入,要么更新。除非有无关的错误,否则这两种结果中的一种是可以保证的。如果冲突源自另一个事务,而该事务的效果尚未对 INSERT
可见,那么 UPDATE
子句将影响该行,即使通常情况下该行没有任何版本对命令可见。
在读已提交模式下,带有 ON CONFLICT DO NOTHING
子句的 INSERT
可能会因为另一个事务的效果尚未对 INSERT
快照可见而导致插入操作未进行。
MERGE
允许用户指定 INSERT
、UPDATE
和 DELETE
子命令的各种组合。包含 INSERT
和 UPDATE
子命令的 MERGE
命令看起来类似于带有 ON CONFLICT DO UPDATE
子句的 INSERT
,但不保证 INSERT
或 UPDATE
会发生。如果 MERGE
尝试执行 UPDATE
或 DELETE
,并且该行已被并发更新,但连接条件对于当前目标和当前源元组仍然成立,那么 MERGE
的行为将与 UPDATE
或 DELETE
命令相同,并对该行更新版本执行其操作。然而,由于 MERGE
可以指定多个操作并且这些操作可以是条件性的,因此从第一个操作开始,每个操作的条件都会在行的更新版本上重新评估,即使原本匹配的操作在操作列表中出现得更晚。另一方面,如果该行被并发更新,导致连接条件失败,那么 MERGE
将接下来评估命令的 NOT MATCHED BY SOURCE
和 NOT MATCHED [BY TARGET]
操作,并执行第一个成功的操作。如果该行被并发删除,那么 MERGE
将评估命令的 NOT MATCHED [BY TARGET]
操作,并执行第一个成功的操作。如果 MERGE
尝试执行 INSERT
,并且存在唯一索引且一行重复插入,则会引发唯一性冲突错误;MERGE
不会尝试通过重新评估 MATCHED
条件来避免此类错误。
由于上述规则,更新命令可能会看到不一致的快照:它可以看到并发更新命令对同一行(它试图更新的行)的影响,但看不到这些命令对数据库中其他行的影响。这种行为使得读已提交模式不适合涉及复杂搜索条件的命令;然而,它对于更简单的场景来说是合适的。例如,考虑将 100 美元从一个账户转到另一个账户
BEGIN; UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345; UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534; COMMIT;
如果另一个事务并发地尝试更改账户 7534 的余额,我们显然希望第二个语句从账户行的更新版本开始。因为每个命令只影响预定的行,允许它看到行的更新版本不会造成任何令人烦恼的不一致。
更复杂的用法可能会在读已提交模式下产生不良结果。例如,考虑一个 DELETE
命令,它操作的数据正在被另一个命令添加和删除其限制条件,例如,假设 website
是一个两行的表,website.hits
分别是 9
和 10
BEGIN; UPDATE website SET hits = hits + 1; -- run from another session: DELETE FROM website WHERE hits = 10; COMMIT;
DELETE
将不起作用,即使在 UPDATE
之前和之后都存在 website.hits = 10
的行。发生这种情况是因为跳过了预更新的行值 9
,并且当 UPDATE
完成并且 DELETE
获取锁时,新行值不再是 10
而是 11
,这不再匹配条件。
由于读已提交模式为每个命令启动一个新的快照,该快照包含截至该时刻所有已提交的事务,因此同一事务中的后续命令无论如何都会看到已提交的并发事务的影响。这里的问题在于一个单个命令是否看到数据库的完全一致的视图。
读已提交模式提供的部分事务隔离对于许多应用程序来说是足够的,并且该模式快速且易于使用;然而,它并非对所有情况都足够。执行复杂查询和更新的应用程序可能需要比读已提交模式提供的更严格的一致性数据库视图。
可重复读 (Repeatable Read) 隔离级别只看到事务开始前已提交的数据;它永远看不到并发事务在事务执行期间未提交的数据或已提交的更改。(然而,每个查询都可以看到其自身事务中先前执行的更新的效果,即使这些更新尚未提交。)这比标准的SQL对此隔离级别的要求更强,并且除了串行化异常外,可以防止 表 13.1 中描述的所有现象。如上所述,这在标准中是允许的,标准只描述了每个隔离级别必须提供的最低保护。
此级别与读已提交不同之处在于,可重复读事务中的查询看到的是事务中第一个非事务控制语句开始时的快照,而不是事务中当前语句开始时的快照。因此,在单个事务中,连续的 SELECT
命令看到相同的数据,即它们看不到在自己的事务开始后、由其他事务提交的更改。
使用此级别的应用程序必须准备好因串行化失败而重试事务。
UPDATE
、DELETE
、MERGE
、SELECT FOR UPDATE
和 SELECT FOR SHARE
命令在搜索目标行方面与 SELECT
的行为相同:它们只会找到在事务开始时已提交的目标行。但是,当找到这样的目标行时,它可能已被另一个并发事务更新(或删除或锁定)。在这种情况下,可重复读事务将等待第一个更新事务提交或回滚(如果它仍在进行中)。如果第一个更新程序回滚,则其效果将被撤销,可重复读事务可以继续更新最初找到的行。但是,如果第一个更新程序提交(并且实际上更新或删除了该行,而不仅仅是锁定它),那么可重复读事务将被回滚,并显示以下消息:
ERROR: could not serialize access due to concurrent update
因为可重复读事务无法修改或锁定在可重复读事务开始后被其他事务更改的行。
当应用程序收到此错误消息时,应中止当前事务并从头开始重试整个事务。第二次执行时,事务将看到先前已提交的更改作为其初始数据库视图的一部分,因此在将新行版本用作新事务更新的起点时,逻辑上没有冲突。
请注意,只有更新事务可能需要重试;只读事务永远不会有串行化冲突。
可重复读模式提供了对每个事务看到稳定的数据库视图的严格保证。然而,此视图不一定总是与同一级别并发事务的某个串行(一次一个)执行一致。例如,即使是此级别的只读事务也可能看到显示批次已完成的控制记录,但由于读取了控制记录的早期版本,而没有看到该批次中逻辑上属于的详细记录之一。尝试在此隔离级别下运行的事务来强制执行业务规则,在没有仔细使用显式锁来阻止冲突事务的情况下,可能无法正确工作。
可重复读隔离级别是使用一种在学术数据库文献和一些其他数据库产品中称为快照隔离 (Snapshot Isolation) 的技术实现的。与使用传统锁定技术(会降低并发性)的系统相比,可能会观察到行为和性能上的差异。一些其他系统甚至可能提供可重复读和快照隔离作为具有不同行为的独立隔离级别。区分这两种技术的允许现象直到 SQL 标准制定之后才被数据库研究人员正式化,并且超出了本手册的范围。有关详细信息,请参阅[berenson95]。
在 PostgreSQL 版本 9.1 之前,请求串行化事务隔离级别提供的行为与此处描述的完全相同。为了保留遗留的串行化行为,现在应该请求可重复读。
串行化 (Serializable) 隔离级别提供了最严格的事务隔离。此级别模拟所有已提交事务的串行事务执行;就好像事务是逐个串行执行的,而不是并发执行的。然而,与可重复读级别一样,使用此级别的应用程序必须准备好因串行化失败而重试事务。事实上,此隔离级别的工作方式与可重复读完全相同,不同之处在于它还监控可能导致并发串行化事务集执行的行为与所有可能的串行(一次一个)执行不一致的情况。这种监控不会引入除可重复读已有的阻塞之外的任何阻塞,但监控会带来一些开销,并且检测可能导致串行化异常的情况将触发串行化失败。
例如,假设一个名为 mytab
的表,最初包含
class | value -------+------- 1 | 10 1 | 20 2 | 100 2 | 200
假设串行化事务 A 计算
SELECT SUM(value) FROM mytab WHERE class = 1;
然后将结果(30)插入到 class
为 = 2
的新行中的 value
列。同时,串行化事务 B 计算
SELECT SUM(value) FROM mytab WHERE class = 2;
并得到结果 300,然后将其插入到 class
为 = 1
的新行中的 value
列。然后两个事务都尝试提交。如果在可重复读隔离级别下运行其中任何一个事务,那么两个事务都会被允许提交;但是,由于不存在一致的结果的串行执行顺序,使用串行化事务将允许一个事务提交,并将另一个事务回滚,并显示以下消息:
ERROR: could not serialize access due to read/write dependencies among transactions
这是因为如果 A 在 B 之前执行,B 将计算总和 330,而不是 300,同样,另一种顺序将导致 A 计算出不同的总和。
当依赖串行化事务来防止异常时,重要的是,从永久用户表中读取的任何数据在读取它的事务成功提交之前都不能被认为是有效的。即使对于只读事务,这也成立,只是可延迟 (deferrable) 只读事务中读取的数据在读取时就被认为是有效的,因为这样的事务会一直等到它能够获得一个保证没有此类问题的快照,然后才开始读取任何数据。在所有其他情况下,应用程序不得依赖于事务中读取的结果(该事务后来中止);相反,它们应该重试事务直到成功。
为了保证真正的串行化,PostgreSQL 使用谓词锁定 (predicate locking),这意味着它会保留锁,这些锁可以帮助它确定如果一个写入在先,它本可以对并发事务的读取结果产生什么影响。在 PostgreSQL 中,这些锁不会引起任何阻塞,因此不会导致死锁。它们用于识别和标记并发串行化事务之间的依赖关系,这些依赖关系在某些组合下可能导致串行化异常。相比之下,想要确保数据一致性的读已提交或可重复读事务可能需要对整个表加锁,这可能会阻塞其他用户使用该表,或者它可以使用 SELECT FOR UPDATE
或 SELECT FOR SHARE
,这些操作不仅可能阻塞其他事务,还会导致磁盘访问。
像大多数其他数据库系统一样,PostgreSQL 中的谓词锁是基于事务实际访问的数据。这些锁将显示在 pg_locks
系统视图中,其 mode
为 SIReadLock
。在事务过程中,查询执行期间获取的特定锁将取决于查询使用的计划,并且多个更细粒度的锁(例如,元组锁)可能会合并成更少更粗粒度的锁(例如,页面锁)。一个 READ ONLY
事务在完成前可能能够释放其 SIRead 锁,如果它检测到无法再发生可能导致串行化异常的冲突。事实上,READ ONLY
事务通常可以在启动时建立这一事实并避免获取任何谓词锁。如果您显式请求 SERIALIZABLE READ ONLY DEFERRABLE
事务,它将阻塞直到能够建立此事实。(这是唯一串行化事务会阻塞但可重复读事务不会阻塞的情况。)另一方面,SIRead 锁通常需要保留到事务提交之后,直到重叠的读写事务完成。
一致地使用串行化事务可以简化开发。任何一组成功提交的并发串行化事务的效果都将与它们逐个串行执行的效果相同这一保证意味着,如果您可以证明一个单独编写的事务在单独运行时会执行正确的操作,那么您就可以确信它在任何串行化事务的混合体中都会执行正确的操作,即使不知道其他事务可能会做什么,或者它将无法成功提交。重要的是,使用此技术的环境应具有处理串行化失败(始终以 SQLSTATE 值 '40001' 返回)的通用方法,因为要精确预测哪些事务可能导致读/写依赖关系并需要回滚以防止串行化异常将非常困难。读/写依赖关系的监控是有成本的,同样,重新启动因串行化失败而终止的事务也是有成本的,但与使用显式锁和 SELECT FOR UPDATE
或 SELECT FOR SHARE
的成本和阻塞相比,在某些环境中,串行化事务是最佳性能选择。
虽然 PostgreSQL 的串行化事务隔离级别只允许并发事务在能够证明存在产生相同效果的串行执行顺序时提交,但它并不总是能防止会出现与真正串行执行不符的错误。特别是,即使在显式检查键是否存在后再尝试插入的情况下,也可能会看到由与重叠串行化事务的冲突引起的唯一约束违例。可以通过确保所有插入潜在冲突键的串行化事务都显式检查它们是否可以首先执行此操作来避免这种情况。例如,设想一个应用程序询问用户一个新的键,然后通过尝试选择它来检查它是否已存在,或者通过选择现有键的最大值并加一来生成新键。如果某些串行化事务在不遵循此协议的情况下直接插入新键,则即使在并发事务的串行执行中不会发生这种情况,也可能报告唯一约束违例。
为了在依赖串行化事务进行并发控制时获得最佳性能,应考虑以下问题:
尽可能将事务声明为 READ ONLY
。
控制活动连接的数量,如有必要,使用连接池。这始终是一个重要的性能考虑因素,但在使用串行化事务的繁忙系统中尤其重要。
不要在一个事务中包含比完整性目的所需更多的内容。
不要让连接长时间处于“事务中空闲”状态。配置参数 idle_in_transaction_session_timeout 可用于自动断开长时间未响应的会话。
消除由于串行化事务提供的保护而不再需要的显式锁、SELECT FOR UPDATE
和 SELECT FOR SHARE
。
当系统由于谓词锁表内存不足而被迫将多个页面级谓词锁合并到一个关系级谓词锁中时,可能会导致串行化失败率增加。您可以通过增加 max_pred_locks_per_transaction、max_pred_locks_per_relation 和/或 max_pred_locks_per_page 来避免这种情况。
顺序扫描总是需要关系级谓词锁。这可能导致串行化失败率增加。通过降低 random_page_cost 和/或增加 cpu_tuple_cost 来鼓励索引扫描可能会有所帮助。请务必权衡事务回滚和重试的任何减少与查询执行时间的总体变化。
串行化隔离级别是使用一种在学术数据库文献中称为串行化快照隔离 (Serializable Snapshot Isolation) 的技术实现的,该技术通过添加对串行化异常的检查来构建快照隔离。与使用传统锁定技术的其他系统相比,可能会观察到行为和性能上的差异。有关详细信息,请参阅[ports12]。
如果您在文档中发现任何不正确之处、与您在使用特定功能时的体验不符之处,或者需要进一步澄清之处,请使用此表单报告文档问题。