为何PostgreSQL中使用ctid实现乐观锁会失败?
为什么PostgreSQL的ctid不能用于实现乐观锁?
核心原因是ctid是行的物理存储标识,而非逻辑版本号,它的更新逻辑和乐观锁的需求完全不匹配:
1. ctid的本质与更新行为
ctid表示行在磁盘上的物理位置(格式为(块号, 行号)),只有当行被物理移动时(比如触发TOAST存储、VACUUM操作导致的行迁移、表膨胀引发的重排),ctid才会改变。而绝大多数常规UPDATE操作(比如仅更新普通字段)不会移动行的物理位置,此时ctid保持不变。
在你的测试场景中:
- 窗口A执行UPDATE后,行的物理位置没有变化,ctid依然是
(0,1) - 窗口B的UPDATE条件包含
ctid=(0,1),当窗口A提交事务后,窗口B的事务继续执行时,当前行的ctid仍与条件匹配,因此UPDATE执行成功,导致rest_count被重复修改为-1,违背了乐观锁的预期。
2. 自定义version列的工作逻辑
自定义version列是逻辑版本标识,它的行为完全可控:每次执行UPDATE时,会主动将version值递增(比如SET version = version + 1),同时将原始version值作为UPDATE条件的一部分。
在测试2中:
- 窗口A执行UPDATE后,version从1变为2并提交
- 窗口B的UPDATE条件是
version=1,此时行的实际version已经是2,条件不匹配,因此返回UPDATE 0,不会修改数据,完美实现乐观锁的冲突检测。
总结
- ctid仅适用于定位行的物理位置(比如底层维护、数据修复场景),不能作为乐观锁的版本标识,因为它的变化依赖行的物理移动,而非逻辑更新。
- 实现乐观锁的正确方式是使用自定义的version列(或timestamp列,注意timestamp的精度问题),通过主动递增版本号并在UPDATE条件中校验版本值来实现冲突检测。
内容的提问来源于stack exchange,提问作者Michelle Liao
相关产品推荐
相关产品推荐

