一个关于Postgres Read Committed隔离级别的问题

Posted by CCH on February 23, 2020

问题

在看PostgreSQL9.6官方文档 第十三章13.2.1节时看到文档提到Read Committed的隔离级别在一些复杂的场景下会导致的问题,下面贴一些原文

More complex usage can produce undesirable results in Read Committed mode. For example, con- sider a DELETE command operating on data that is being both added and removed from its restriction criteria by another command, e.g., assume website is a two-row table with website.hits equaling 9 and 10:

BEGIN;
UPDATE website SET hits = hits + 1;
-- run from another session:  DELETE FROM website WHERE hits = 10;
COMMIT;

复现

T1事务去update, 让每一行的hits 都加1。 同时, T2事务去删除hits为10的行。乍一看, 事务T2一定可以删除至少一行,因为不管update执行前后,都会有一行hits== 10.

但是执行的结果是, T2不能删除任何一行。

T1 T2
begin; begin;
update website set hits = hits + 1;  
--update 2 delete from website where hits = 10;
commit; --delete 0
  commit;

分析

原因是,在Read Committed隔离级别中, 如果一个UPDATA/DELETE/SELECT FOR UPDATE/SELECT FOR SHARE指令(上文情景,就是T2的delete指令), 发现一个符合自己where条件的数据行R时,会去看是否有另外一个事务(在上文的情景,就是T1)已经锁住了数据行R(用于T1的更新操作),如果是,那么T2事务进行等待,直到T1 commit或rollback,T2再继续进行,判断R是否被修改:

  1. 若T1commit,
    • 1-1 如果R被T1修改,那么T2的delete将看见R被更新后的值,同时会重新检查R的新值是否符合该命令的WHERE条件, 如果符合,将使用R1的新值作为搜索结果,如果不符合,将忽略R的新值。
    • 1-2 如果R被T1删除, 那么T2的delete就会忽略R。
  2. 若T1 rollback, 那么T2继续在R上执行delete操作。

所以,在上文情景下, T2的delete遍历website表,由于是read committed隔离级别,所以它看到的行仍然是两行hits=9,hits=10(因为T1还没commit), 于是它跳过hits=9的行,准备delete hits=10的行。此时,T2检查看是否有另外一个事务(T1)锁住了数据行(hits=10的行),确实被锁住了,于是等待T1 commit之后,T2重新检查这一行是否符合where条件,结果不符合,也跳过了这以后,所以最后delete 0行。

这个场景说明,read committed隔离级别中,事务的每一个SQL命令执行前都会去获取一个snapshot,单个SQL的执行过程中,看到的数据,其实也会不一致。上述过程中T2跳过的hits=9的行,属于T1 commit前的数据,而第二次跳过的行,却已经是T1 commith后的数据了。

源码分析

delete命令的调用栈

heap_delete()
ExecDelete()
ExecutePlan()
...
..

在执行函数heap_delete(), 会用HeapTupleSatisfiesUpdate()查看当前tuple的状态,在上述场景的T2 delete hits==10这一行时, HeapTupleSatisfiesUpdate()会放回HeapTupleBeingUpdated. 表示这一行正在被其他事务更新。于是T2调用LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); 等锁, 直到T1 commit之后,T2得到锁,从heap_delete中返回tuple的状态(此时为HeapTupleUpdated)给ExecDelete()。ExecDelete()看到此时为HeapTupleUpdated时,会去重新EvalPlanQual 重新检查更新后的tuple是否还符合where条件,若不符合,直接跳过这行的delete