丢失更新(Lost Update),又被称作写-写冲突(ww-conflict),是事务并发更新同一行时所发生的异常,REPEATABLE READ和SERIALIZABLE隔离等级必须阻止该异常的出现。 本节将会介绍PostgreSQL是如何防止丢失更新的,并举一些例子来说明。
5.8.1 并发UPDATE
命令的行为
执行UPDATE
命令时,内部实际上调用了ExecUpdate
函数。 ExecUpdate
的伪代码如下所示:
伪代码:
ExecUpdate
(1) FOR row in 本UPDATE命令待更新的所有行集 (2) WHILE true /* 第一部分 */ (3) IF 目标行 正在 被更新 THEN (4) 等待 更新目标行的事务 结束(提交或中止) (5) IF (更新目标行的事务已提交) AND (当前事务隔离级别是 可重复读或可串行化) THEN (6) 中止当前事务 /* 以先更新者为准 */ ELSE (7) 跳转步骤(2) END IF /* 第二部分 */ (8) ELSE IF 目标行 已经 被另一个并发事务所更新 THEN (9) IF (当前事务的隔离级别是 读已提交 ) THEN (10) 更新目标行 ELSE (11) 中止当前事务 /* 先更新者为准 */ END IF /* 第三部分 */ /* 目标行没有被修改过,或者被一个 已经结束 的事务所更新 */ ELSE (12) 更新目标行 END IF END WHILE END FOR
- 获取被本
UPDATE
命令更新的每一行,并对每一行依次执行下列操作。- 重复以下过程,直到目标行更新完成,或本事务中止。
- 如果目标行正在 被更新则进入步骤(4),否则进入步骤(8)。
- 等待正在更新目标行的事务结束,因为PostgreSQL在SI中使用了以先更新者为准(first-updater-win) 的方案。
- 如果更新目标行的事务已经提交,且当前事务的隔离等级为可重复读或可串行化则进入步骤(6),否则进入步骤(7)。
- 中止本事务,以防止丢失更新。(因为另一个事务已经对目标行进行了更新并提交)
- 跳转回步骤(2),并对目标行进行新一轮的更新尝试。
- 如果目标行已被 另一个并发 事务所更新则进入步骤(9),否则进入步骤(12)。
- 如果当前事务的隔离级别为读已提交 则进入步骤(10),否则进入步骤(11)。
- 更新目标行,并回到步骤(1),处理下一条目标行。
- 中止当前事务,以防止丢失更新。
- 更新目标行,并回到步骤(1),因为目标行尚未被修改过,或者虽然已经被更新,但更新它的事务已经结束。已终止的事务更新,即存在写写冲突。
此函数依次为每个待更新的目标行执行更新操作。 它有一个外层循环来更新每一行,而内部while循环则包含了三个分支,分支条件如图5.11所示。
图5.11 ExecUpdate
内部的三个部分
- 目标行正在被更新,如图5.11所示
“正在被更新”意味着该行正在被另一个事务同时更新,且另一个事务尚未结束。在这种情况下,当前事务必须等待更新目标行的事务结束,因为PostgreSQL的SI实现采用以先更新者为准(first-updater-win) 的方案。例如,假设事务
Tx_A
和Tx_B
同时运行,且Tx_B
尝试更新某一行;但Tx_A
已更新了这一行,且仍在进行中。在这种情况下Tx_B
会等待Tx_A
结束。 在更新目标行的事务提交后,当前事务的更新操作将完成等待继续进行。如果当前事务处于READ COMMITTED
隔离等级,则会更新目标行;而若处于REPEATABLE READ
或SERIALIZABLE
隔离等级时,当前事务则会立即中止,以防止丢失更新。 - 目标行已经 被另一个并发事务所更新,如图5.11所示
当前事务尝试更新目标元组,但另一个并发事务已经更新了目标行并提交。在这种情况下,如果当前事务处于
READ COMMITTED
级别,则会更新目标行;否则会立即中止以防止丢失更新。 - 没有冲突,如图5.11所示 当没有冲突时,当前事务可以直接更新目标行。
以先更新者为准 / 以先提交者为准
PostgreSQL基于SI的并发控制机制采用以先更新者为准(first-updater-win) 方案。 相反如下一节所述,PostgreSQL的SSI实现使用以先提交者为准(first-commiter-win) 方案。
5.8.2 例子
以下是三个例子。 第一个和第二个例子展示了目标行正在 被更新时的行为,第三个例子展示了目标行已经被更新的行为。
例1
事务Tx_A
和Tx_B
更新同一张表中的同一行,它们的隔离等级均为READ COMMITTED
。
Tx_A |
Tx_B |
---|---|
START TRANSACTION ISOLATION LEVEL READ COMMITTED; |
|
START TRANSACTION |
START TRANSACTION ISOLATION LEVEL READ COMMITTED; |
START TRANSACTION |
|
UPDATE tbl SET name = 'Hyde'; |
|
UPDATE 1 |
|
UPDATE tbl SET name = 'Utterson'; |
|
↓*-- 本事务进入阻塞状态,等待Tx_A 完成* |
|
COMMIT; |
↓*-- Tx_A 提交,阻塞解除* |
UPDATE 1 |
Tx_B
的执行过程如下:
- 在执行
UPDATE
命令之后,Tx_B
应该等待Tx_A
结束,因为目标元组正在被Tx_A
更新(ExecUpdate
步骤4) - 在
Tx_A
提交后,Tx_B
尝试更新目标行(ExecUpdate
步骤7) - 在
ExecUpdate
内循环第二轮中,目标行被Tx_B
更新(ExecUpdate
步骤2,8,9,10)。
例2
Tx_A
和Tx_B
更新同一张表中的同一行,它们的隔离等级分别为读已提交和可重复读。
Tx_A |
Tx_B |
---|---|
START TRANSACTION ISOLATION LEVEL READ COMMITTED; |
|
START TRANSACTION |
START TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
START TRANSACTION |
|
UPDATE tbl SET name = 'Hyde'; |
|
UPDATE 1 |
|
UPDATE tbl SET name = 'Utterson'; |
|
↓*-- 本事务进入阻塞状态,等待Tx_A 完成* |
|
COMMIT; |
↓*-- Tx_A 提交,阻塞解除* |
ERROR:couldn't serialize access due to concurrent update |
Tx_B
的执行过程如下:
Tx_B
在执行UPDATE
命令后阻塞,等待Tx_A
终止(ExecUpdate
步骤4)。- 当
Tx_A
提交后,Tx_B
会中止以解决冲突。因为目标行已经被更新,且当前事务Tx_B
的隔离级别为可重复读(ExecUpdate
步骤5,6)。
例3
Tx_B
(可重复读)尝试更新已经被Tx_A
更新的目标行,且Tx_A
已经提交。 在这种情况下,Tx_B
会中止(ExecUpdate
中的步骤2,8,9,11)。
Tx_A |
Tx_B |
---|---|
START TRANSACTION ISOLATION LEVEL READ COMMITTED; |
|
START TRANSACTION |
START TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
START TRANSACTION |
|
UPDATE tbl SET name = 'Hyde'; |
|
UPDATE 1 |
|
COMMIT; |
|
UPDATE tbl SET name = 'Utterson'; |
|
ERROR:couldn't serialize access due to concurrent update |
下一节:从版本9.1开始,可串行化快照隔离(SSI)已经嵌入到快照隔离(SI)中,用以实现真正的可串行化隔离等级。SSI解释起来过于复杂,故本书仅解释其概要,详细信息请参阅文献。