必须开启数据库死锁日志:mysql需配置innodb_print_all_deadlocks=on,postgresql需启用log_lock_waits=on;django自身不记录死锁细节,仅依赖底层数据库日志才能准确定位循环等待、加锁顺序冲突等根因。

查死锁得先让数据库把日志吐出来
Django 本身不记录死锁细节,真正能定位问题的是底层数据库。MySQL 和 PostgreSQL 的死锁日志开关默认通常是关的,不手动开,你永远只能看到 OperationalError: (1213, 'Deadlock found when trying to get lock...') 这种模糊报错。
以 MySQL 为例,必须在配置里加:innodb_print_all_deadlocks = ON(写进 my.cnf 的 [mysqld] 段),并确保 innodb_status_output = ON。重启后死锁信息会输出到错误日志(路径看 SHOW VARIABLES LIKE 'log_error';)。
PostgreSQL 则需设置:log_lock_waits = on、deadlock_timeout = 1s(不能太小,否则干扰正常等待),日志里会出现类似 process A waits for ShareLock on transaction 12345; blocked by process B 的链路。
- 别依赖 Django 的
LOGGING配置——它看不到事务底层锁竞争 - 应用日志里只记了异常抛出点,不是死锁发生点;真正冲突的 SQL 往往在不同请求、不同视图里交叉执行
- 如果用 RDS(如阿里云 RDS、AWS RDS),控制台通常提供「死锁日志」独立开关,比改参数更直接
用 select_for_update() 时最容易撞上死锁
Django 的 select_for_update() 是常见死锁源头,尤其当多个视图按不同顺序对同一组行加锁,或锁范围超出预期时。
比如两个请求并发执行:
Order.objects.select_for_update().filter(id__in=[101, 202]).order_by('id') # 先锁 101,再锁 202<br>Order.objects.select_for_update().filter(id__in=[202, 101]).order_by('id') # 先锁 202,再锁 101
只要两个事务执行时间重叠,就构成经典循环等待。
- 务必统一加锁顺序:始终用
.order_by('pk')或明确字段,避免依赖数据库默认排序(可能随索引/统计信息变化) - 避免在
select_for_update()后做耗时操作(如调用外部 API、渲染模板),锁持有时间越长,冲突概率越高 - 不要在事务外调用
select_for_update()—— Django 会自动开启事务,但若没显式transaction.atomic包裹,可能被中间件或信号打断,导致锁滞留 - 慎用
select_for_update(nowait=True):它不解决死锁,只是把死锁异常换成DatabaseError: Lock wait timeout exceeded,掩盖真实问题
从慢查询日志反推潜在锁竞争点
死锁常和长事务、未提交更新共存。即使没触发死锁,长时间持有的锁也会提高死锁概率。MySQL 的 slow_query_log + long_query_time = 0.1 能抓到那些“卡住”的 UPDATE/DELETE。
重点盯这些模式:
-
UPDATE account SET balance = balance - 100 WHERE user_id = 123—— 没索引的WHERE条件会导致全表扫描加锁,扩大锁范围 -
SELECT ... FOR UPDATE后跟了复杂计算或循环,但没及时save()或commit - Django Admin 中批量操作(如勾选多行点击「删除」)触发的隐式事务,可能因数据量大而拖长锁时间
用 EXPLAIN 看执行计划,确认是否走了索引;对高频更新字段(如 status、updated_at)补上索引,能显著缩小锁粒度。
用 django-debug-toolbar 只能辅助,不能替代数据库层分析
Toolbar 显示了当前请求的 SQL 和耗时,但它不显示锁状态、不记录已释放的事务、也不关联其他并发请求。你看到一条 UPDATE 执行了 800ms,但不知道它是等锁 790ms 还是真慢。
真正有用的是结合:
- 数据库的
INFORMATION_SCHEMA.INNODB_TRX(MySQL)或pg_stat_activity+pg_locks(PostgreSQL)实时查活跃事务和锁等待 - 在怀疑的视图里加临时日志:
logger.info("About to select_for_update on order %s", order_id),配合时间戳,和数据库日志对齐 - 用
django.db.connection.queries在开发环境打印原始 SQL,确认 Django 生成的语句是否意外扩大了 WHERE 范围(比如__in传空列表变成全表扫描)
死锁不是单次代码 bug,而是多个并发路径在特定时序下的组合结果。最麻烦的 case 往往藏在信号(post_save)、中间件、或第三方包的隐式数据库操作里——这些地方连 select_for_update() 都没写,却悄悄开启了事务。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











