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

Django事务中使用select_for_update函数出现死锁问题求助

死锁原因分析

结合你的代码、REPEATABLE-READ隔离级别以及并发场景,死锁的触发逻辑及关键问题点如下:

核心触发逻辑

两个线程的事务围绕同一条Boo数据的排他锁形成了循环等待(单条数据看似不会触发死锁,但结合事务时序和锁特性,实际存在触发可能):

典型触发时序

假设线程A执行func_change_data,线程B执行func__read:

  1. 线程B先启动,进入事务后执行Boo.objects.select_for_update().get(pk=xxx, target_branch='master', stage='stage_1'):
    • 此时数据stage为stage_1,满足查询条件,线程B成功获取这条数据的排他锁,随后进入time.sleep(10),持续持有锁不释放。
  2. 线程A启动,进入事务后先执行time.sleep(10),未尝试获取任何锁。
  3. 线程Asleep结束,执行Boo.objects.select_for_update(skip_locked=True).filter(pk=xxx):
    • 若你的数据库版本不支持skip_locked(比如MySQL 5.7及更早版本),该参数会被忽略,线程A会阻塞等待线程B释放锁。
    • 若此时线程B的事务还在等待线程A持有的其他锁(比如代码未体现的关联表操作),就会形成循环等待,触发死锁。

另一种可能的触发场景

  1. 线程A先启动,进入事务后执行select_for_update(skip_locked=True).filter(pk=xxx),成功获取锁(此时stage为stage_1),随后进入sleep(10)。
  2. 线程B启动,进入事务后执行select_for_update().get(...):在REPEATABLE-READ隔离级别下,线程B读取的是事务启动时的快照,看不到线程A未提交的stage修改,因此会尝试获取原快照中满足stage='stage_1'的数据锁,进入阻塞等待。
  3. 线程Asleep结束后执行boo.save()更新stage,若此时线程A的事务还需要获取其他资源锁(比如关联表锁),而该锁恰好被线程B持有,就会形成循环等待,触发死锁。

关键问题点

  • skip_locked=True有效性存疑:如果数据库版本不支持该参数,线程A会无意义地阻塞等待锁,增加死锁概率。
  • REPEATABLE-READ快照特性:线程B的事务依赖启动时的快照,即使数据被修改仍会尝试获取原条件下的锁,导致持续等待。
  • 锁持有时间过长:time.sleep(10)大幅延长了锁的持有时长,放大了并发冲突的可能性。

解决方案建议

  • 确认数据库版本支持skip_locked,确保select_for_update(skip_locked=True)生效,避免不必要的锁等待。
  • 缩短事务锁持有时间,将sleep这类非核心操作移到事务外部,减少锁占用时长。
  • 若func__read不需要修改数据,移除select_for_update改用普通查询;若必须加锁,可添加skip_locked=True或设置锁超时。
  • 检查是否存在关联表操作,确保多线程的锁获取顺序一致,避免循环等待。

内容的提问来源于stack exchange,提问作者scanf3

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 11:37:13