使用SELECT FOR UPDATE致死锁是否正常?咨询最优规避策略
是的,这种看似逻辑完全一致的并发事务出现死锁,其实是PostgreSQL里的正常预期情况——别觉得意外,这里藏着锁机制里容易被忽略的细节。
为什么会出现死锁?
你可能以为两个事务都在争抢同一行的排他锁,应该是串行等待,但实际上PostgreSQL的SELECT ... FOR UPDATE在执行时,锁的申请不是一步到位的:
- 每个事务会先申请事务ID级的共享锁(用来跟踪事务间的依赖关系);
- 然后再去申请目标行的排他行锁。
当两个事务几乎同时发起时,就可能出现这种循环等待:
- 事务A拿到了事务B的事务ID共享锁,等着事务B释放行锁;
- 事务B也拿到了事务A的事务ID共享锁,等着事务A释放行锁。
这种循环依赖就触发了死锁,PostgreSQL的死锁检测器会很快检测到并终止其中一个事务,这也就是你在pg_locks里看到两个SELECT互相阻塞的原因。
规避死锁的最佳策略
根据你的业务场景,可以选择以下几种方案:
用
SKIP LOCKED跳过锁定行
如果业务允许跳过已经被处理的行,直接用SELECT files.data FROM files WHERE files.file_id = 123 LIMIT 1 FOR UPDATE SKIP LOCKED;。这个选项会让查询直接忽略被其他事务锁住的行,不会进入等待队列,从根源上避免死锁。适合批量处理、任务分发这类场景。使用 advisory lock 强制串行化
如果必须确保每个请求都能处理目标行,可以借助PostgreSQL的 advisory lock(咨询锁)机制,在事务开始前先锁定对应的file_id:BEGIN; -- 先获取咨询锁,同一file_id同一时间只能被一个事务持有 SELECT pg_advisory_lock(123); SELECT files.data FROM files WHERE files.file_id = 123 LIMIT 1 FOR UPDATE; UPDATE files SET ... WHERE files.file_id = 123; -- 释放咨询锁 SELECT pg_advisory_unlock(123); COMMIT;这样能确保同一时间只有一个事务能处理该
file_id的行,彻底避免死锁。压缩事务时长
虽然你的事务已经很短,但尽量把非必要操作移出事务边界,确保SELECT FOR UPDATE之后立刻执行UPDATE,减少锁的持有时间,降低死锁触发的概率。用
NOWAIT快速失败并重试
如果业务能接受重试逻辑,可以用SELECT ... FOR UPDATE NOWAIT;,当行被锁定时会直接抛出错误,应用层捕获错误后重试整个事务即可。这种方式适合对响应速度要求高的场景,避免长时间等待。
内容的提问来源于stack exchange,提问作者Yoneshiro

