问题报告 纠错本页面

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命令操作的数据 正在被另外一个命令添加到,或者移除出它的约束条件。比如,假设website是拥有website.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涉及到的阻断, 可串行化事务在这种环境下是性能最好的选择。

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

Warning

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