TheSQL标准定义了四个事务隔离级别。最严格的是可序列化,标准中的一段话定义了它,保证一组可序列化事务的任何并发执行都会产生与按某种顺序一次运行它们相同的效果。其他三个级别是根据现象定义的,这些现象是由并发事务之间的交互产生的,并且在每个级别都不允许发生。标准指出,由于可序列化的定义,在那个级别不可能发生这些现象。(这不足为奇——如果事务的效果必须与一次运行的效果一致,你怎么能看到任何由交互引起的现象呢?)
在不同级别禁止的现象是
SQL 标准和 PostgreSQL 实现的事务隔离级别在 表 13.1 中进行了描述。
表 13.1. 事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 序列化异常 |
---|---|---|---|---|
读取未提交 | 允许,但在 PG 中不适用 | 可能 | 可能 | 可能 |
读取已提交 | 不可能 | 可能 | 可能 | 可能 |
可重复读取 | 不可能 | 不可能 | 允许,但在 PG 中不适用 | 可能 |
可序列化 | 不可能 | 不可能 | 不可能 | 不可能 |
在 PostgreSQL 中,您可以请求四个标准事务隔离级别中的任何一个,但内部只实现了三个不同的隔离级别,即 PostgreSQL 的“读取未提交”模式的行为类似于“读取已提交”。这是将标准隔离级别映射到 PostgreSQL 的多版本并发控制架构的唯一合理方法。
该表还显示 PostgreSQL 的可重复读取实现不允许幻读。这在 SQL 标准下是可以接受的,因为该标准指定了在特定隔离级别下必须不发生的异常情况;更高的保证是可以接受的。以下小节详细介绍了可用隔离级别的行为。
要设置事务的事务隔离级别,请使用命令 SET TRANSACTION。
某些 PostgreSQL 数据类型和函数在事务行为方面有特殊规则。特别是,对序列所做的更改(因此对使用 serial
声明的列的计数器)会立即对所有其他事务可见,并且如果进行更改的事务中止,则不会回滚。请参阅 第 9.17 节 和 第 8.1.4 节。
读取已提交是 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
条件来避免此类错误。
由于上述规则,更新命令可能会看到不一致的快照:它可以看到并发更新命令对其尝试更新的同一行的影响,但它看不到这些命令对数据库中其他行的影响。此行为使“读已提交”模式不适合涉及复杂搜索条件的命令;但是,它对于更简单的情况来说恰到好处。例如,考虑使用以下交易更新银行余额:
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
等于 9
和 10
:
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
,这不再符合条件。
因为“读已提交”模式为每个命令启动一个新的快照,其中包含所有在该时刻之前提交的事务,所以在同一事务中的后续命令无论如何都会看到已提交的并发事务的效果。上面讨论的重点是 单个 命令是否会看到数据库的绝对一致的视图。
“读已提交”模式提供的部分事务隔离足以满足许多应用程序的需求,并且此模式使用起来快速而简单;但是,它并不适用于所有情况。执行复杂查询和更新的应用程序可能需要比“读已提交”模式提供的更严格一致的数据库视图。
可重复读隔离级别仅查看在事务开始之前提交的数据;它永远看不到未提交的数据或在事务执行期间由并发事务提交的更改。(但是,每个查询都会看到在其自身事务中执行的先前更新的效果,即使它们尚未提交。)这比
SQL此隔离级别的标准所要求的保证要强,并且可以防止表 13.1中描述的所有现象,除了序列化异常。如上所述,标准明确允许这样做,标准仅描述每个隔离级别必须提供的最小保护。
此级别与“读已提交”的不同之处在于,可重复读事务中的查询将看到快照,该快照是在事务中第一个非事务控制语句的开始时创建的,而不是在事务中当前语句的开始时创建的。因此,单个事务中的连续 SELECT
命令会看到相同的数据,即,它们看不到在它们自己的事务启动后提交的其他事务进行的更改。
使用此级别的应用程序必须准备好因序列化失败而重试事务。
UPDATE
、DELETE
、MERGE
、SELECT FOR UPDATE
和 SELECT FOR SHARE
命令在搜索目标行方面的行为与 SELECT
相同:它们将仅找到在事务开始时已提交的目标行。但是,在找到这样的目标行时,它可能已经被另一个并发事务更新(或删除或锁定)。在这种情况下,可重复读事务将等待第一个更新事务提交或回滚(如果它仍在进行中)。如果第一个更新者回滚,则其效果将被抵消,并且可重复读事务可以继续更新最初找到的行。但是,如果第一个更新者提交(并且实际上更新或删除了行,而不仅仅是锁定它),则可重复读事务将被回滚并显示以下消息:
ERROR: could not serialize access due to concurrent update
因为可重复读事务不能修改或锁定在可重复读事务开始后由其他事务更改的行。
当应用程序收到此错误消息时,应中止当前事务并从头开始重试整个事务。第二次运行时,事务会将先前提交的更改视为其数据库初始视图的一部分,因此使用该行的新版本作为新事务更新的起点在逻辑上没有冲突。
请注意,只有更新事务可能需要重试;只读事务永远不会发生序列化冲突。
可重复读模式提供了严格的保证,即每个事务都会看到数据库的完全稳定的视图。但是,此视图不一定总是与同一级别并发事务的某些串行(一次一个)执行一致。例如,即使此级别的只读事务也可能会看到控制记录已更新,以显示批处理已完成,但没有看到逻辑上属于该批处理的其中一条详细记录,因为它读取了控制记录的早期版本。在没有仔细使用显式锁来阻止冲突事务的情况下,尝试通过在此隔离级别运行的事务来强制执行业务规则可能无法正确工作。
可重复读隔离级别是使用在学术数据库文献和某些其他数据库产品中被称为 快照隔离 的技术实现的。与使用传统的锁定技术(减少并发)的系统相比,可能会观察到行为和性能方面的差异。某些其他系统甚至可能提供可重复读和快照隔离作为具有不同行为的不同隔离级别。在开发 SQL 标准后,数据库研究人员才正式确定了区分这两种技术所允许的现象,这些现象超出了本手册的范围。有关完整处理,请参阅 [berenson95]。
在 PostgreSQL 9.1 版本之前,对可序列化事务隔离级别的请求提供的行为与此处描述的行为完全相同。要保留传统的“可序列化”行为,现在应请求“可重复读”。
可序列化隔离级别提供了最严格的事务隔离。此级别模拟所有已提交事务的串行事务执行;就好像事务是一个接一个地串行执行的,而不是并发执行的。但是,与“可重复读”级别一样,使用此级别的应用程序必须准备好因序列化失败而重试事务。实际上,此隔离级别的工作方式与“可重复读”完全相同,只是它还会监视可能使并发的可序列化事务集以与这些事务的所有可能的串行(一次一个)执行不一致的方式运行的条件。这种监视不会引入超出“可重复读”中存在的任何阻塞,但是监视会有一些开销,并且检测到可能导致 序列化异常 的条件将触发 序列化失败。
例如,考虑一个名为 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 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,鼓励使用索引扫描可能会有所帮助。务必权衡事务回滚和重启的减少与查询执行时间的总体变化。
可序列化隔离级别是使用学术数据库文献中称为可序列化快照隔离的技术实现的,该技术在快照隔离的基础上增加了对序列化异常的检查。与其他使用传统锁定技术的系统相比,可能会观察到行为和性能方面的一些差异。有关详细信息,请参见 [ports12]。
如果您在文档中发现任何不正确、与您特定功能的使用经验不符或需要进一步澄清的地方,请使用此表格报告文档问题。