问题
在看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是否被修改:
- 若T1commit,
- 1-1 如果R被T1修改,那么T2的delete将看见R被更新后的值,同时会重新检查R的新值是否符合该命令的WHERE条件, 如果符合,将使用R1的新值作为搜索结果,如果不符合,将忽略R的新值。
- 1-2 如果R被T1删除, 那么T2的delete就会忽略R。
- 若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