Django事务中使用select_for_update函数出现死锁问题求助
死锁原因分析
结合你的代码、REPEATABLE-READ隔离级别以及并发场景,死锁的触发逻辑及关键问题点如下:
核心触发逻辑
两个线程的事务围绕同一条Boo数据的排他锁形成了循环等待(单条数据看似不会触发死锁,但结合事务时序和锁特性,实际存在触发可能):
典型触发时序
假设线程A执行func_change_data,线程B执行func__read:
- 线程B先启动,进入事务后执行
Boo.objects.select_for_update().get(pk=xxx, target_branch='master', stage='stage_1'):- 此时数据
stage为stage_1,满足查询条件,线程B成功获取这条数据的排他锁,随后进入time.sleep(10),持续持有锁不释放。
- 此时数据
- 线程A启动,进入事务后先执行
time.sleep(10),未尝试获取任何锁。 - 线程A
sleep结束,执行Boo.objects.select_for_update(skip_locked=True).filter(pk=xxx):- 若你的数据库版本不支持
skip_locked(比如MySQL 5.7及更早版本),该参数会被忽略,线程A会阻塞等待线程B释放锁。 - 若此时线程B的事务还在等待线程A持有的其他锁(比如代码未体现的关联表操作),就会形成循环等待,触发死锁。
- 若你的数据库版本不支持
另一种可能的触发场景
- 线程A先启动,进入事务后执行
select_for_update(skip_locked=True).filter(pk=xxx),成功获取锁(此时stage为stage_1),随后进入sleep(10)。 - 线程B启动,进入事务后执行
select_for_update().get(...):在REPEATABLE-READ隔离级别下,线程B读取的是事务启动时的快照,看不到线程A未提交的stage修改,因此会尝试获取原快照中满足stage='stage_1'的数据锁,进入阻塞等待。 - 线程A
sleep结束后执行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
相关产品推荐
相关产品推荐

