死锁排查与解决
问题
线上出现死锁告警或大量锁等待,如何快速定位和解决?
答案
一、死锁原理
死锁条件:两个(或更多)事务互相持有对方需要的锁,形成循环等待。
MySQL InnoDB 处理:自动检测死锁,回滚较小的事务(undo log 量少的一方)。
二、排查流程
三、查看死锁信息
-- 查看最近的死锁详情
SHOW ENGINE INNODB STATUS\G
-- 关注 LATEST DETECTED DEADLOCK 部分
-- 查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- MySQL 8.0
SELECT * FROM performance_schema.data_lock_waits;
-- 查看当前所有锁
SELECT * FROM performance_schema.data_locks;
-- 查看长时间未提交的事务
SELECT * FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10;
四、常见死锁场景
场景 1:相反顺序更新
-- 事务 A:先锁 id=1,再锁 id=2
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 获得 id=1 的行锁
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待 id=2 的行锁
-- 事务 B:先锁 id=2,再锁 id=1
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 获得 id=2 的行锁
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 等待 id=1 的行锁 → 死锁!
解决:统一加锁顺序,始终按 id 从小到大。
async function transfer(fromId: number, toId: number, amount: number) {
// 统一按 ID 排序,避免死锁
const [first, second] = fromId < toId
? [fromId, toId]
: [toId, fromId];
await db.transaction(async (tx) => {
// 始终先锁较小 ID
await tx.query('SELECT * FROM accounts WHERE id = ? FOR UPDATE', [first]);
await tx.query('SELECT * FROM accounts WHERE id = ? FOR UPDATE', [second]);
await tx.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId]);
await tx.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId]);
});
}
场景 2:间隙锁死锁
-- 表中无 id=5 的记录,隔离级别 RR
-- 事务 A
BEGIN;
SELECT * FROM t WHERE id = 5 FOR UPDATE; -- 获得间隙锁 (4, 6)
INSERT INTO t (id) VALUES (5); -- 等待事务 B 的间隙锁
-- 事务 B
BEGIN;
SELECT * FROM t WHERE id = 5 FOR UPDATE; -- 也获得间隙锁 (4, 6)(间隙锁兼容)
INSERT INTO t (id) VALUES (5); -- 等待事务 A 的间隙锁 → 死锁!
解决:使用 INSERT ... ON DUPLICATE KEY UPDATE 代替先查后插。
场景 3:索引缺失导致锁升级
-- 无 status 索引时,WHERE status = 'pending' 会锁全表
UPDATE orders SET status = 'processing'
WHERE status = 'pending' AND user_id = 100;
-- InnoDB 扫描所有行 → 锁住大量无关行 → 与其他事务冲突
解决:添加索引 (status, user_id),缩小锁范围。
五、预防策略
| 策略 | 做法 |
|---|---|
| 固定加锁顺序 | 多表/多行操作时按固定顺序 |
| 缩小事务范围 | 减少事务中的操作,快速提交 |
| 避免长事务 | 设置 innodb_lock_wait_timeout = 5 |
| 添加合适索引 | 确保 UPDATE/DELETE 使用索引 |
| 降低隔离级别 | RC 级别无间隙锁,减少死锁概率 |
常见面试问题
Q1: MySQL 如何检测死锁?
答案:
InnoDB 维护一个等待图(Wait-for Graph):
- 每个事务是图中的节点
- 如果事务 A 等待事务 B 的锁,则 A→B 连一条边
- 定期检查图中是否有环
- 发现环 → 选择回滚代价最小的事务
检测频率很高(每次锁等待时触发),所以死锁通常能立即发现。
Q2: RR 和 RC 隔离级别下死锁的区别?
答案:
| 维度 | RR(可重复读) | RC(读已提交) |
|---|---|---|
| 间隙锁 | 有 | 无 |
| 死锁概率 | 更高 | 更低 |
| 锁范围 | 行锁 + 间隙锁 + Next-Key Lock | 仅行锁 |
| 适用建议 | MySQL 默认级别 | 高并发写入场景可考虑 |
互联网业务中很多团队选择 RC 隔离级别以减少死锁。
Q3: 如何处理线上频繁死锁?
答案:
- 紧急止血:确认 InnoDB 自动回滚了死锁事务,应用层做重试
- 定位热点:
SHOW ENGINE INNODB STATUS找到死锁 SQL - 优化方案:
- 加索引减少锁范围
- 统一加锁顺序
- 拆分大事务
- 评估是否可以降到 RC
- 监控预警:监控
innodb_row_lock_waits指标