You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用SELECT FOR UPDATE致死锁是否正常?咨询最优规避策略

同一行SELECT FOR UPDATE并发死锁:原因与解决方案

是的,这种看似逻辑完全一致的并发事务出现死锁,其实是PostgreSQL里的正常预期情况——别觉得意外,这里藏着锁机制里容易被忽略的细节。

为什么会出现死锁?

你可能以为两个事务都在争抢同一行的排他锁,应该是串行等待,但实际上PostgreSQL的SELECT ... FOR UPDATE在执行时,锁的申请不是一步到位的:

  1. 每个事务会先申请事务ID级的共享锁(用来跟踪事务间的依赖关系);
  2. 然后再去申请目标行的排他行锁。

当两个事务几乎同时发起时,就可能出现这种循环等待:

  • 事务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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 09:05:23