SQL 标准定义了四个事务隔离级别。最严格的是可串行化, 标准将它定义为:一组可串行化事务的任意并发执行,都保证与按照某种顺序一次只运行一个事务的效果相同。 其他三个级别则按现象定义,这些现象源自并发事务之间的相互作用,而在各自级别上这些现象不得发生。 标准指出,由于可串行化的定义,这些现象在该级别下都不可能出现。 (这并不奇怪,如果事务的效果必须与一次只运行一个事务保持一致,又怎么可能看到由相互作用引起的任何现象呢?)
在各个级别上被禁止的现象有:
SQL 标准以及 PostgreSQL 实现的事务隔离级别见 Table 13.1。
Table 13.1. 事务隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 串行化异常 |
|---|---|---|---|---|
| 读未提交 | 允许,但 PostgreSQL 中不会发生 | 可能 | 可能 | 可能 |
| 读已提交 | 不可能 | 可能 | 可能 | 可能 |
| 可重复读 | 不可能 | 不可能 | 允许,但 PostgreSQL 中不会发生 | 可能 |
| 可串行化 | 不可能 | 不可能 | 不可能 | 不可能 |
在 PostgreSQL 中,你可以请求四种标准事务隔离级别中的任意一种,但在内部只实现了三种不同的隔离级别;也就是说,PostgreSQL 的读未提交模式与读已提交模式行为相同。这是因为,要把标准隔离级别映射到 PostgreSQL 的多版本并发控制架构上,这是唯一合理的做法。
该表还显示,PostgreSQL 的可重复读实现不允许幻读。这在 SQL 标准下是可以接受的, 因为标准规定的是某些隔离级别下哪些异常不得发生;提供更高的保证也是允许的。 可用隔离级别的行为将在下面各小节中详细说明。
要设置事务的事务隔离级别,请使用命令 SET TRANSACTION。
某些 PostgreSQL 数据类型和函数在事务行为方面有特殊规则。特别是,对序列的修改(以及使用 serial 声明的列所对应的计数器)会立即对所有其他事务可见,并且即使执行该修改的事务中止,这些修改也不会被回滚。见 Section 9.17 和 Section 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 的快照不可见。同样,这种情况只会出现在读已提交模式下。
由于上述规则,更新命令可能看到一个不一致的快照:它能够看到并发更新命令在它试图更新的同一行上的效果, 却看不到这些命令对数据库中其他行的影响。这种行为使得读已提交模式不适合涉及复杂搜索条件的命令; 不过,对于较简单的场景它恰到好处。例如,考虑将 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,因此不再匹配条件。
因为在读已提交模式中,每个命令都从一个新快照开始,而该快照包含截至当时已提交的所有事务, 所以同一事务中的后续命令无论如何都会看到并发事务已提交的效果。上面真正的问题在于, 单个命令是否能看到数据库的绝对一致视图。
读已提交模式所提供的部分事务隔离对于许多应用来说已经足够,而且这种模式既快速又容易使用。 不过,它并不能满足所有场景。执行复杂查询和更新的应用,可能需要比读已提交模式所提供的更严格一致的数据库视图。
可重复读隔离级别只能看到事务开始前已提交的数据;它看不到未提交的数据, 也看不到事务执行期间并发事务提交的更改。(不过,每个查询都能看到其自身事务中先前执行的更新效果, 即使这些更新尚未提交。)这比 SQL 标准对该隔离级别的要求更强,并且除了串行化异常之外, 能够防止 Table 13.1 中描述的所有现象。正如前面提到的, 这正是标准明确允许的,因为标准只描述每个隔离级别必须提供的最低保护。
这一点与读已提交不同:在可重复读事务中,查询看到的是该事务中第一条非事务控制语句开始时的快照, 而不是事务中当前语句开始时的快照。因此,在单个事务中的连续 SELECT 命令会看到相同的数据,也就是说,它们不会看到在自身事务开始后由其他事务提交的更改。
使用这一隔离级别的应用必须准备好在发生串行化失败时重试事务。
UPDATE,DELETE,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]。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。