13.2. 事务隔离

SQL标准定义了四个级别的事务隔离。最严格的是串行化,它是通过标准来定义的,也就是说, 保证一组可序列化事务的并发执行以产生同样顺序依次运行它们的同一效果。 其他三个层次是通过现象术语被定义,导致并发事务之间的相互作用,这不应该发生在每个级别中。 标准定义归因于序列化的定义,这些现象不可能在这一水平上(这毫不奇怪--如果事务的影响必须与已运行的一个保持一致,你怎么能看到通过相互作用引起的现象呢?

各个级别不希望发生的现象是:

脏读

一个事务读取了另一个未提交事务写入的数据。

不可重复读

一个事务重新读取前面读取过的数据,发现该数据已经被另一个已提交事务修改。

幻读

一个事务重新执行一个查询,返回一套符合查询条件的行,发现这些行因为其它最近提交的事务而发生了改变。

这四种隔离级别和对应的行为在表Table 13-1里描述。

Table 13-1. 标准SQL事务隔离级别

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可串行化 不可能 不可能 不可能

在PostgreSQL里,你可以请求四种可能的事务隔离级别中的任意一种。但是在内部, 实际上只有三种独立的隔离级别,分别对应读已提交,可重复读和可串行化。如果你选择了读未提交的级别, 实际上你用的是读已提交,在重复读的PostgreSQL执行时,幻读是不可能的, 所以实际的隔离级别可能比你选择的更严格。这是 SQL 标准允许的:四种隔离级别只定义了哪种现像不能发生, 但是没有定义那种现像一定发生。PostgreSQL只提供两种隔离级别的原因是, 这是把标准的隔离级别与多版本并发控制架构映射相关的唯一合理方法。可用的隔离级别的行为在下面小节里描述。

要设置一个事务的隔离级别,使用SET TRANSACTION命令。

Important: 一些PostgreSQL数据类型和函数关于事务行为有特定的规则。 尤其是,序列变化(因此列数通过serial声明)对于所有其他的事务是立即可见的, 如果事务改变终止,则不进行回退。参见Section 9.16Section 8.1.4

13.2.1. 读已提交隔离级别

读已提交是PostgreSQL里的缺省隔离级别。当一个事务运行在这个隔离级别时, SELECT查询(没有FOR UPDATE/SHARE子句)只能看到查询开始之前已提交的数据而无法看到未提交的数据或者在查询执行期间其它事务已提交的数据 。实际上,SELECT 查询看到一个在查询开始运行的瞬间该数据库的一个快照。 不过,SELECT看得见其自身所在事务中前面更新执行结果。即使它们尚未提交。请注意, 在同一个事务里两个相邻的SELECT命令可能看到不同的快照,因为其它事务会在第一个SELECT执行期间提交。

UPDATE, DELETE, SELECT FOR UPDATESELECT FOR SHARE命令在搜索目标行时的行为和SELECT一样: 它们只能找到在命令开始的时候已经提交的行。不过, 这样的目标行在被找到的时候可能已经被其它并发事务更新、删除、锁住。在这种情况下, 即将进行的更新将等待第一个事务提交或者回滚(如果它还在处理)。如果第一个事务回滚, 那么它的作用将被忽略,而第二个事务将继续更新最初发现的行。如果第一个事务提交, 那么如果第一个事务删除了该行,则第二个事务将忽略该行, 否则它将试图在该行的已更新的版本上施加它的操作。系统将重新计算命令搜索条件(WHERE子句), 看看该行已更新的版本是否仍然符合搜索条件。如果符合,则第二个事务从该行的已更新版本开始继续其操作。 如果是SELECT FOR UPDATESELECT 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命令数据操作 通过另外一个命令的限制标准中被添加或者删除等,假设websitewebsite.hits 等同于910的两行表格。

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,它不再符合标准。

因为在读已提交模式里,每个新的命令都是从一个新的快照开始的, 而这个快照包含所有到该时刻为止已提交的事务, 因此同一事务中后面的命令将看到任何已提交的其它事务的效果。 这里关心的问题是在单个命令里是否看到数据库里绝对一致的视图。

读已提交模式提供的部分事务隔离对于许多应用而言是足够的,并且这个模式速度快,使用简单。 不过,对于做复杂查询和更新的应用, 可能需要保证数据库有比读已提交模式更加严格的一致性视图。

13.2.2. 可重复读隔离级别

可重复读隔离级别仅仅看到事务开始之前提交的数据,它不能看到在并发事务执行期间未提交的数据和已提交的改变。 (然而,查询看到在自身事务中执行的先前更新的效果,即使它们没有被提交)。比为这一隔离级别的SQL标准需求来说,这是一个更强烈的保证。 避免所有在Table 13-1描述的现象。正如上述所提及的,这是通过标准允许的, 这仅仅描述必须提供的每个隔离级别的最低限度保护。

这个级别和读已提交级别是不一样的。重复读事务中的查询看到的是事务开始时的快照, 而不是该事务内部当前查询开始时的快照,这样, 同一个事务内部后面的SELECT命令总是看到同样的数据等,它们没有看到通过 自身事务开始之后提及的其他事务做出的改变。

使用这个级别的应用必须准备好重试事务,因为串行化失败。

UPDATE, DELETE, SELECT FOR UPDATESELECT FOR SHARE在搜索目标行时的行为和SELECT一样: 它们将只寻找在事务开始的时候已经提交的目标行。但是, 这样的目标行在被发现的时候可能已经被另外一个并发的事务更新、删除、锁住。在这种情况下, 可串行化的事务将等待第一个正在更新的事务提交或者回滚(如果它仍然在处理中)。如果第一个事务回滚, 那么它的影响将被忽略,而这个可串行化的就可以继续更新它最初发现的行。 但是如果第一个事务被提交了(并且实际上更新或者删除了该行,而不只是锁住它)那么可串行化事务将回滚, 并返回下面信息:

ERROR:  could not serialize access due to concurrent update

因为一个可串行化的事务在开始之后不能更改或者锁住被其它事务更改过的行。

当应用收到这样的错误信息时,它应该退出当前的事务然后从新开始进行整个事务。第二次运行时, 该事务看到的快照将包含前一次已提交的修改,所以不会有逻辑冲突。

请注意只有更新事务才需要重试,只读事务从来没有串行化冲突。

可重复读事务级别提供了严格的保证:每个事务都看到一个完全一致的数据库视图。然而, 这种观点也不一定总是与(一次一个)同一级别的并发事务连续执行一致。 例如,即使在这个级别上的一个只读事务可以看到控制记录更新显示一批已经完成,但 不能看到一批逻辑部分的详细记录, 因为它读取较早版本的控制记录。如果不仔细使用显式锁来阻止冲突事务,通过运行在这个隔离级别上的事务尝试执行业务规则是不能正常工作的。

Note: 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, 如果B在A之前执行,那么 A 计算出的总和也会不同。

当依赖于可串行化事务阻止异常时,来自永久用户表读取的任何数据不被认为是有效的,直到事务读取的成功提交为止。 这对于只读事务是真的,除了在可延期的只读事务中的数据读是有效的。 因为这样一个事务等待直到它可以在开始读取任何数据之前获得一个快照保证这些问题是自由的。 在所有其他情况下,应用不依赖于结果读,期间事务之后被停止;相反,他们应该重启事务直到成功为止。

为了保证PostgreSQL真正可串行化使用谓词锁定。 这意味着当写对于并发事务的先前读结果有重大影响时,它使锁决定首先运行。 在PostgreSQL这些锁不造成任何阻塞,因此可以导致僵局。 它们被用来识别和标记并发序列化事务中的依赖关系,其中一定的组合可导致序列化异常。 相反,读已提交或者可重复读取的事务要确保数据的一致性可能需要获取整个表锁, 它可以阻止其他尝试使用该表的用户,也可以使用SELECT FOR UPDATE或者SELECT FOR SHARE,这不仅可以阻止其他事务而且可能导致磁盘访问。

PostgreSQL中的谓词锁,像其他大多数数据库系统一样, 基于通过事务实际访问的数据,这些显示在pg_locks 系统视图中,并带有SIReadLock模式。 查询执行期间特定的锁的获得将取决于使用的查询计划。以及事务进程防止用于跟踪锁定的内存耗尽期间的多个细粒度锁(例如,元组锁)可以组合成较少的粗粒度的锁(例如,页锁)。 只读事务可以在完成之前释放SIRead锁,如果它检测到没有冲突仍然发生,这可能会导致一系列的异常。事实上, 只读事务会经常建立启动事实,并且避免采取任何谓词锁。如果你明确要求SERIALIZABLE READ ONLY DEFERRABLE事务,这将阻塞直到它可以建立这一事实。 (这是唯一情况,可序列化事务块可以但可重复读事务不行。)另一方面,SIRead锁经常需要保持过去的事务提交,直到重叠读写事务完成。

可序列化事务一致性的使用可以简化开发。如果他们每次运行一个,保证任何一组并发序列化事务会具有相同的效果。 这意味着如果你能证明单一事务,作为书面的,当自己运行时将做正确事情, 你可以有信心它会在任何组合可序列化事务中做正确的事,即使没有任何有关那些其他事务的消息。 使用这种技术的环境中有一个处理序列化失败的方法是很重要的(它总是返回'40001'的SQLSTATE值), 因为它很难准确预测,事务可能有助于读/写依赖并且需要回滚防止序列化异常。读/写依赖的监控是有成本的,正如序列化失败而终止之后进行事务重新启动, 但权衡成本和使用显式锁以及SELECT FOR UPDATE或者SELECT FOR SHARE涉及到的阻断, 可序列化事务在这种环境下是性能最好的选择。

为了最佳性能,当为并发控制依赖于可串行化事务时,应该考虑这些问题:

  • 可能时作为只读声明事务。

  • 如果需要,可以使用连接池,控制活动连接数。这是一个重要性能的考虑,但是在使用可串行化 事务的繁忙系统中尤其重要。

  • 比起需要完整性目的来说不要将更多的东西放到单一事务中。

  • 不要让连接在"闲置的事务"中停留超过需要的时间。

  • 消除显示锁,SELECT FOR UPDATESELECT FOR SHARE不再需要,因为通过可串行化事务自动提供保护。

  • 当系统强制连接多个页级别谓词锁到单一关系级别谓词锁,因为谓词锁表是短期存储, 可能产生可串行化失败率的增加。你可以通过增加max_pred_locks_per_transaction 来避免。

  • 顺序扫描总是需要一个关系级别谓词锁。这可能会导致序列化失败率增加。 这可能有助于鼓励减少random_page_cost 和/或增加cpu_tuple_cost的索引扫描使用。 一定要权衡任何事务回滚的减少,并且重新启动查询执行时间内的任何整体变化。

Warning
序列化事务隔离级别尚未被添加到热备复制目标中 (正如在Section 25.5中描述的)。 严格的隔离级别目前热备方式上支持可重复读。 当在主库上执行所有永久数据库写入可序列化事务中将确保所有的措施将最终达成一致, 运行在备库上的可重复读事务会看到一个过渡状态,与主库上的任何串行执行的可序列化事务不一致。