9.2. 事务隔离

SQL 标准 用三个必须在并行的事务之间避免的现象定义了四个级别的事务隔离。 这些不希望发生的现象是:

脏读(dirty reads)

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

不可重复读(non-repeatable reads)

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

幻读(phantom read)

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

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

Table 9-1. SQL 隔离级别

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
读未提交(Read uncommitted) 可能 可能 可能
读已提交(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能

PostgreSQL 提供读已提交(read committed)和可串行化(serializable)隔离级别。

9.2.1. 读已提交隔离级别

读已提交(Read Committed)PostgreSQL 里的缺省隔离级别。 当一个事务运行在这个隔离级别时, 一个 SELECT 查询只能看到查询开始之前提交的数据而永远 无法看到未提交的数据或者是在查询执行时其他并行的事务提交做的改变。 (不过 SELECT 的确看得见同一次事务中前面更新 的结果.即使它们还没提交也看得到.) 实际上,一个 SELECT 查询看到一个在该查询开始 运行的瞬间该数据库地一个快照。 请注意两个相邻的 SELECT 可能看到不同 的数据,哪怕它们是在同一个事务里,因为其它事务会在第一个 SELECT执行的时候提交.

UPDATEDELETE, 或者 SELECT FOR UPDATE 在搜索目标行的时候的行为和 SELECT 一样:它们只能找到在查询开始的时候已经提交 的行。 不过,这样的目标行在被找到的时候可能已经被其它并发的事务更新(或者删除,或者标记为更新的)。 在这种情况下,即将进行的更新将等待第一个更新事务提交或者回滚(如果它还在处理)。 如果第一个更新回滚,那么它的作用将被忽略,而第二个更新者将继续更新最初发现的行。 如果第一个更新者提交,那么如果第一个更新者删除了该行,则第二个更新者将忽略该行, 否则它将视图在该行的已更新的版本上施加它的操作。系统将重新计算查询搜索条件 (WHERE 子句),看看该行已更新的办不那是否仍然符合搜索条件。如果是, 则第二个更新继续其操作,从该行的已更新版本开始。

因为上面的规则,正在更新的规则可能会看到不一致的快找 --- 它们可以看到 影响它们试图更新的并发更新查询的效果,但是它们看不到那些查询对数据库 里其它行的作用。这样的行为令读已提交模式不适合用于哪种涉及复杂搜索条件 的查询。不过,它对于简单的情况而言是正确的。比如,假设我们用类似下面这样 的查询更新银行余额:

BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;

如果两个并发事务试图修改帐号 12345 的余额,那我们很明显希望第二个 事务是从帐户行的已经更新过的版本上进行更新。因为每个查询只是影响 一个已经决定了的行,因此让它看到更新后的版本不会导致任何不一致的 问题。

因为在读已提交模式里,每个新的查询都是从一个新的快照开始的,而这个 快照包含所有到该时刻为止已经提交的事务,因此同一个事务里的后面的 查询将看到任何已提交的并发事务的效果。这里要考虑的问题是我们在 一个查询里是否看到数据库里绝对一致的视图。

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

9.2.2. 可串行化隔离级别

可串行化(Serializable) 提供最严格的事务隔离。 这个级别模拟串行的事务执行, 就好象事务将被一个接着一个那样串行的,而不是并行的执行。 不过,使用这个级别的应用必须准备在串行化失败的时候重新发动事务.

当一个事务处于可串行化级别, 一个 SELECT 查询只能看到在事务开始之前提交的数据而永远看不到未提交的 数据或事务执行中其他并行事务提交的修改。 (不过,SELECT 的确看得到同一次事务中 前面的更新的效果.即使事务还没有提交也一样.) 这个行为和读已提交级别是不太一样的,它的 SELECT 看到的是 该事务开始时的快照,而不是该事务内部当前查询开始时的快照. 这样,一个事务内部后面的 SELECT 总是看到同样的数据。

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

ERROR:  Can't serialize access due to concurrent update

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

当应用收到这样的错误信息时,它应该退出当前的事务然后从头开始重新 进行整个事务.第二次运行时,该事务看到的前一次提交的修改是该数据库 初始的样子中的一部分,所以把新版本的行作为新事务更新的起点不会有 逻辑冲突.

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

可串行化事务级别提供了严格的保证:每个事务都看到一个完全一致的数据库 的视图.不过,如果并行更新令数据库不能维持串行执行的样子,那么应用 必须准备重试事务。因为重做复杂的事务的开销可能是非常可观的,所以我们 只建议在更新查询中包含足够复杂的逻辑,在读已提交级别中可能导致错误 的结果的情况下才使用. 最常见的是,可串行化模式只是在这样的情况下是必要的:一个事务连续做若干个查询, 而这几个查询必须看到数据库完全一样的视图。