如何预防或减少Django应用操作MySQL时出现的死锁问题
Django+MySQL高并发场景Payment表插入死锁解决方案
核心根因确认
你推测外键为死锁诱因的判断符合该场景的特征:
插入Payment实例时,MySQL会自动对to_member、from_member两个外键关联的Member表行加共享锁(S锁),高并发下如果两个事务的加锁顺序相反(比如事务1先锁用户A再锁用户B,事务2先锁用户B再锁用户A),就会触发循环等待条件,直接产生死锁。
可落地解决方案
- 强制统一加锁顺序(根除90%以上此类死锁)
所有涉及多Member行关联的操作,统一按Member ID升序的顺序加排他锁,再执行Payment创建,从根本上消除循环等待的死锁条件,代码示例如下:from django.db import transaction with transaction.atomic(): # 统一按ID升序排序,保证所有事务加锁顺序完全一致 if from_member.id > to_member.id: from_member, to_member = to_member, from_member # 对两个关联Member行加排他锁,阻塞其他事务同时操作这两行 Member.objects.select_for_update().filter(id__in=[from_member.id, to_member.id]) # 再执行Payment创建逻辑 payment = Payment.objects.create( to_member=to_member, to_name=to_member.username, from_member=from_member, from_name=from_member.username, description="Resubscription", type='resubscription', amount=5.00, currency=from_member, processor='e-wallet' ) - 调整MySQL事务隔离级别
默认的REPEATABLE-READ(可重复读)隔离级别会开启间隙锁,大幅提升死锁概率,可将MySQL全局/会话隔离级别调整为READ-COMMITTED(读已提交),自动关闭间隙锁,该调整对绝大多数业务的一致性无影响,可直接在生产环境生效。 - 缩短事务持有锁的时间
检查Payment创建逻辑是否包裹在大事务中,尽可能将非原子性操作移出事务,缩短锁持有时长,降低锁冲突概率。不要随意调整innodb_lock_wait_timeout等锁配置参数,默认参数足够支撑业务,乱改容易引发系统整体锁死。 - 新增死锁重试兜底机制
可添加死锁异常自动重试逻辑,避免用户侧感知报错,代码示例如下:import time from django.db import OperationalError from functools import wraps def deadlock_retry(max_retries=3): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): retries = 0 while retries < max_retries: try: return func(*args, **kwargs) except OperationalError as e: if "Deadlock found" in str(e) and retries < max_retries - 1: retries += 1 time.sleep(0.1 * retries) continue raise return wrapper return decorator # 用法 @deadlock_retry(max_retries=3) def create_resubscription_payment(from_member, to_member): # 上述事务+加锁+创建Payment逻辑
内容的提问来源于stack exchange,提问作者Daniel Askwith
相关产品推荐
相关产品推荐

