pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
SQL标准根据并发事务之间必须防止的三个现象,定义了四个事务隔离级别。这些不受欢迎的现象是:
四种事务隔离级别及其对应行为见 表 13.1。
表 13.1. SQL Transaction Isolation Levels
| Isolation Level | Dirty Read | Nonrepeatable Read | Phantom Read |
|---|---|---|---|
| Read uncommitted | Possible | Possible | Possible |
| Read committed | Not possible | Possible | Possible |
| Repeatable read | Not possible | Not possible | Possible |
| Serializable | Not possible | Not possible | Not possible |
在PostgreSQL中,你可以请求四种标准事务隔离级别中的任意一种。但在内部,实际上只有两种不同的隔离级别,分别对应读已提交和可串行化这两个级别。当你选择读未提交级别时,实际得到的是读已提交;而当你选择可重复读时,实际得到的是可串行化,因此实际的隔离级别可能比你选择的更严格。这是 SQL 标准所允许的:这四个隔离级别只定义了哪些现象不允许发生,并没有定义哪些现象必须发生。PostgreSQL之所以只提供两种隔离级别,是因为要把标准隔离级别映射到多版本并发控制架构上,这是唯一合理的做法。可用隔离级别的行为将在下面各小节中详细说明。
要设置事务的事务隔离级别,请使用命令 SET TRANSACTION。
读已提交是PostgreSQL中默认的隔离级别。当事务使用这一隔离级别时,SELECT查询(不带FOR UPDATE/SHARE子句)只能看到查询开始之前已经提交的数据;它既看不到未提交的数据,也看不到查询执行期间并发事务提交的更改。实际上,SELECT查询看到的是该查询开始运行瞬间的数据库快照。不过,SELECT能看到其自身事务中先前执行的更新效果,即使这些更新尚未提交。还要注意,即使两个连续的SELECT命令位于同一个事务中,如果其他事务在第一个SELECT执行期间提交了更改,这两个命令也可能看到不同的数据。
UPDATE、DELETE、SELECT FOR UPDATE 和 SELECT FOR SHARE 命令在搜索目标行时的行为与 SELECT 相同: 它们只会找到在命令开始时已经提交的目标行。不过,当找到这样的目标行时, 它可能已经被其他并发事务更新、删除或者锁定。在这种情况下,即将执行更新的事务会等待第一个更新事务提交或回滚 (如果它仍在进行中)。如果第一个更新事务回滚,那么它的作用将被撤销,第二个更新事务就可以继续更新最初找到的行。 如果第一个更新事务提交了,那么若该行已被第一个更新者删除,第二个更新事务就会忽略该行; 否则,第二个更新者将尝试在该行的已更新版本上应用自己的操作。 该命令的搜索条件(WHERE 子句)会被重新计算,以判断该行的已更新版本是否仍然符合搜索条件。 如果符合,则第二个更新者会基于该行的已更新版本继续执行操作。 对于 SELECT FOR UPDATE 和 SELECT FOR SHARE, 这意味着被锁住并返回给客户端的是该行的已更新版本。
由于上述规则,更新命令可能看到一个不一致的快照:它能够看到并发更新命令在它试图更新的同一行上的效果, 却看不到这些命令对数据库中其他行的影响。这种行为使得读已提交模式不适合涉及复杂搜索条件的命令; 不过,对于较简单的场景它恰到好处。例如,考虑以如下事务更新银行余额:
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; -- 从另一个会话运行: DELETE FROM website WHERE hits = 10; COMMIT;
该 DELETE 将不会产生任何效果,即使在 UPDATE 之前和之后都存在一行 website.hits = 10。这是因为更新前值为 9 的那一行被跳过了,而当 UPDATE 完成并且 DELETE 获得锁时,新行值已经不再是 10 而是 11,因此不再匹配条件。
因为在读已提交模式中,每个命令都从一个新快照开始,而该快照包含截至当时已提交的所有事务, 所以同一事务中的后续命令无论如何都会看到并发事务已提交的效果。上面真正的问题在于, 单个命令是否能看到数据库的绝对一致视图。
读已提交模式所提供的部分事务隔离对于许多应用来说已经足够,而且这种模式既快速又容易使用。 不过,它并不能满足所有场景。执行复杂查询和更新的应用,可能需要比读已提交模式所提供的更严格一致的数据库视图。
可串行化隔离级别提供最严格的事务隔离。该级别模拟串行事务执行,就像事务是一个接一个串行执行而不是并发执行一样。不过,使用该级别的应用必须准备好在发生串行化失败时重试事务。
当事务使用可串行化级别时,SELECT查询只能看到事务开始之前已经提交的数据;它既看不到未提交的数据,也看不到事务执行期间并发事务提交的更改。(不过,查询能看到其自身事务中先前执行的更新效果,即使这些更新尚未提交。)这一点与读已提交不同:可串行化事务中的查询看到的是事务开始时刻的快照,而不是事务中当前查询开始时刻的快照。因此,单个事务中连续的SELECT命令看到相同的数据,也就是说,它们看不到在本事务启动之后由其他事务提交的更改。(这种行为对于报表类应用来说可能非常理想。)
UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命令在搜索目标行方面的行为与SELECT相同:它们只会找到事务开始时刻已提交的目标行。然而,这样的目标行在被找到时,可能已经被另一个并发事务更新(或删除、锁定)。在这种情况下,可串行化事务将等待第一个更新事务提交或回滚(如果仍在进行)。如果第一个更新事务回滚,那么它的作用将被撤销,可串行化事务就可以继续更新最初找到的行。但如果第一个更新事务提交了(并且确实更新或删除了该行,而不仅仅是锁定它),那么可串行化事务将被回滚,并收到以下消息:
ERROR: could not serialize access due to concurrent update
这是因为可串行化事务不能修改或锁定在它开始之后被其他事务更改过的行。
当应用收到这条错误消息时,应当中止当前事务,并从头重试整个事务。 第二次执行时,该事务会把先前已提交的更改视为其初始数据库视图的一部分, 因此以该行的新版本作为新事务更新的起点时,就不存在逻辑冲突。
注意,只有更新事务才可能需要重试;只读事务永远不会发生串行化冲突。
可串行化模式严格保证每个事务看到的都是完全一致的数据库视图。然而,当并发更新使得串行执行的假象无法维持时,应用必须准备好重试事务。由于重做复杂事务的代价可能相当可观,只有当更新事务包含的逻辑复杂到在读已提交模式下可能给出错误结果时,才推荐使用可串行化模式。最常见的情况是,当一个事务要执行多条必须看到相同数据库视图的连续命令时,就需要使用可串行化模式。
“可串行化”执行的直观含义(以及数学定义)是:任何两个成功提交的并发事务,看起来都像严格串行地一个接一个执行过一样——尽管哪一个看起来先发生未必能事先预料。必须认识到,仅仅禁止表 13.1中列出的那些不受欢迎的行为,并不足以保证真正的可串行性;事实上,PostgreSQL的可串行化模式并不保证这个意义上的可串行化执行。作为一个例子,考虑表mytab,它最初包含:
class | value
-------+-------
1 | 10
1 | 20
2 | 100
2 | 200
Suppose that serializable transaction A computes:
SELECT SUM(value) FROM mytab WHERE class = 1;
然后将结果(30)作为新行的value插入,且该行满足class = 2。与此同时,可串行化事务 B 执行以下计算:
SELECT SUM(value) FROM mytab WHERE class = 2;
得到结果 300,并将它插入一个新行,该行满足class = 1。然后两个事务都提交。上面列出的任何不受欢迎的行为都没有发生,然而我们得到的结果,却不可能由任何一种串行顺序产生。如果 A 先于 B 执行,B 计算出的总和将是 330 而不是 300;类似地,另一种执行顺序也会使 A 计算出不同的总和。
要保证真正的数学可串行性,数据库系统必须实现谓词锁,也就是说,一个事务不能插入或修改这样一个行:它会匹配另一个并发事务中某条查询的WHERE条件。例如,一旦事务 A 执行了查询SELECT ... WHERE class = 1,一个采用谓词锁的系统就会禁止事务 B 插入任何 class 为 1 的新行,直到 A 提交为止。[9]这样的锁系统实现起来很复杂,执行起来的代价也极其高昂,因为每个会话都必须了解每个并发事务执行的每条查询的细节。而且这笔巨大的开销大部分都被浪费掉了,因为在实践中,大多数应用并不会做那些可能造成问题的事情。(当然,上面的例子相当人为,也不太可能代表真实的软件。)出于这些原因,PostgreSQL没有实现谓词锁。
在非可串行化执行的可能性构成实际风险的场合,可以通过恰当地使用显式锁定来防止问题。进一步的讨论见后续各节。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。