MySQL 死锁是两个或多个事务互相等待对方释放锁,结果谁也执行不下去。InnoDB 会自动检测死锁并回滚其中一个事务,但频繁死锁会影响性能和稳定性。排查死锁的关键是会读死锁日志,找到事务之间的循环依赖。
死锁是怎么发生的
经典场景:
- 事务 A 先更新行 1,再请求行 2 的锁
- 事务 B 先更新行 2,再请求行 1 的锁
- 互相等待,形成死锁
死锁不一定是代码 bug,很多时候是业务逻辑里操作顺序不一致导致的。
开启死锁日志
InnoDB 的死锁信息默认写在错误日志里。确保日志开启:
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';
如果是 OFF,开启它:
SET GLOBAL innodb_print_all_deadlocks = ON;
永久生效需要在 my.cnf 里加:
[mysqld]
innodb_print_all_deadlocks = 1
读取死锁日志
MySQL 错误日志通常在 /var/log/mysql/error.log 或 /var/lib/mysql/hostname.err。搜索 LATEST DETECTED DEADLOCK:
grep -A 30 "LATEST DETECTED DEADLOCK" /var/log/mysql/error.log
日志里会显示:
- 死锁发生的时间
- 涉及的事务 ID
- 每个事务持有的锁和等待的锁
- MySQL 选择回滚的事务
分析死锁日志
关键信息:
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 123, OS thread handle 123456789, query id 456 localhost user updating
UPDATE accounts SET balance = balance - 100 WHERE id = 1
看两个事务分别持有什么锁、等待什么锁。找到循环等待的两行记录,就是死锁的根源。
解决死锁的方法
1. 统一操作顺序
所有事务按相同顺序访问记录。比如都先更新 id 小的,再更新 id 大的:
UPDATE accounts SET ... WHERE id IN (1, 2) ORDER BY id;
2. 缩短事务长度
事务里不要放不必要的查询和计算。事务越快结束,持有锁的时间越短,死锁概率越低。
3. 使用合适的隔离级别
默认 REPEATABLE READ 锁范围较大。如果业务允许,可以降到 READ COMMITTED:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
4. 加合适的索引
没有索引的 WHERE 条件会触发全表扫描,锁住大量行,增加死锁概率。
监控和预防
查看历史死锁次数:
SHOW ENGINE INNODB STATUS\G
关注 Deadlocks 计数。如果持续增长,说明代码里有系统性的顺序问题。
也可以用 performance_schema 查询最近的锁等待:
SELECT * FROM performance_schema.data_lock_waits\G
死锁排查清单
| 步骤 | 操作 |
|---|---|
| 1 | 开启 innodb_print_all_deadlocks |
| 2 | 在错误日志里找到 LATEST DETECTED DEADLOCK |
| 3 | 定位两个互相等待的事务 |
| 4 | 检查业务代码中操作记录的顺序 |
| 5 | 统一访问顺序或缩短事务长度 |
| 6 | 验证死锁频率是否下降 |
更多 MySQL 运维教程,请访问 fenij.com。
常见问题
问:死锁和锁等待超时有什么区别?
答:死锁是循环等待,InnoDB 会自动检测并回滚一个事务。锁等待超时是一个事务等另一个事务释放锁,但等太久超过了 innodb_lock_wait_timeout,MySQL 报错而不是自动处理。
问:MySQL 回滚哪个事务?
答:InnoDB 通常选择修改行数最少(undo 最少)的事务回滚,这样回滚成本最低。但具体选择可能因版本和场景而异。
问:死锁日志里的事务 ID 能和业务请求对应起来吗?
答:直接对应比较难。建议在应用层给 SQL 加注释(如 /* order_id=12345 */),这样死锁日志里能看到注释,便于定位业务来源。