5.8 防止丢失更新

丢失更新(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 
  1. 获取被本UPDATE命令更新的每一行,并对每一行依次执行下列操作。
  2. 重复以下过程,直到目标行更新完成,或本事务中止。
  3. 如果目标行正在 被更新则进入步骤(4),否则进入步骤(8)。
  4. 等待正在更新目标行的事务结束,因为PostgreSQL在SI中使用了以先更新者为准(first-updater-win) 的方案。
  5. 如果更新目标行的事务已经提交,且当前事务的隔离等级为可重复读或可串行化则进入步骤(6),否则进入步骤(7)。
  6. 中止本事务,以防止丢失更新。(因为另一个事务已经对目标行进行了更新并提交)
  7. 跳转回步骤(2),并对目标行进行新一轮的更新尝试。
  8. 如果目标行已被 另一个并发 事务所更新则进入步骤(9),否则进入步骤(12)。
  9. 如果当前事务的隔离级别为读已提交 则进入步骤(10),否则进入步骤(11)。
  10. 更新目标行,并回到步骤(1),处理下一条目标行。
  11. 中止当前事务,以防止丢失更新。
  12. 更新目标行,并回到步骤(1),因为目标行尚未被修改过,或者虽然已经被更新,但更新它的事务已经结束。已终止的事务更新,即存在写写冲突。

此函数依次为每个待更新的目标行执行更新操作。 它有一个外层循环来更新每一行,而内部while循环则包含了三个分支,分支条件如图5.11所示。

Fig. 5.11. Three internal blocks in ExecUpdate.图5.11 ExecUpdate内部的三个部分

  1. 目标行正在被更新,如图5.11所示 “正在被更新”意味着该行正在被另一个事务同时更新,且另一个事务尚未结束。在这种情况下,当前事务必须等待更新目标行的事务结束,因为PostgreSQL的SI实现采用以先更新者为准(first-updater-win) 的方案。例如,假设事务Tx_ATx_B同时运行,且Tx_B尝试更新某一行;但Tx_A已更新了这一行,且仍在进行中。在这种情况下Tx_B会等待Tx_A结束。 在更新目标行的事务提交后,当前事务的更新操作将完成等待继续进行。如果当前事务处于READ COMMITTED隔离等级,则会更新目标行;而若处于REPEATABLE READSERIALIZABLE隔离等级时,当前事务则会立即中止,以防止丢失更新。
  2. 目标行已经 被另一个并发事务所更新,如图5.11所示 当前事务尝试更新目标元组,但另一个并发事务已经更新了目标行并提交。在这种情况下,如果当前事务处于READ COMMITTED级别,则会更新目标行;否则会立即中止以防止丢失更新。
  3. 没有冲突,如图5.11所示 当没有冲突时,当前事务可以直接更新目标行。
以先更新者为准 / 以先提交者为准

PostgreSQL基于SI的并发控制机制采用以先更新者为准(first-updater-win) 方案。 相反如下一节所述,PostgreSQL的SSI实现使用以先提交者为准(first-commiter-win) 方案。

5.8.2 例子

以下是三个例子。 第一个和第二个例子展示了目标行正在 被更新时的行为,第三个例子展示了目标行已经被更新的行为。

例1

事务Tx_ATx_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的执行过程如下:

  1. 在执行UPDATE命令之后,Tx_B应该等待Tx_A结束,因为目标元组正在被Tx_A更新(ExecUpdate步骤4)
  2. Tx_A提交后,Tx_B尝试更新目标行(ExecUpdate步骤7)
  3. ExecUpdate内循环第二轮中,目标行被Tx_B更新(ExecUpdate步骤2,8,9,10)。

例2

Tx_ATx_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的执行过程如下:

  1. Tx_B在执行UPDATE命令后阻塞,等待Tx_A终止(ExecUpdate步骤4)。
  2. 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