跳到主要内容

死锁排查与解决

问题

线上出现死锁告警或大量锁等待,如何快速定位和解决?

答案

一、死锁原理

死锁条件:两个(或更多)事务互相持有对方需要的锁,形成循环等待。

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)

  1. 每个事务是图中的节点
  2. 如果事务 A 等待事务 B 的锁,则 A→B 连一条边
  3. 定期检查图中是否有
  4. 发现环 → 选择回滚代价最小的事务

检测频率很高(每次锁等待时触发),所以死锁通常能立即发现。

Q2: RR 和 RC 隔离级别下死锁的区别?

答案

维度RR(可重复读)RC(读已提交)
间隙锁
死锁概率更高更低
锁范围行锁 + 间隙锁 + Next-Key Lock仅行锁
适用建议MySQL 默认级别高并发写入场景可考虑

互联网业务中很多团队选择 RC 隔离级别以减少死锁。

Q3: 如何处理线上频繁死锁?

答案

  1. 紧急止血:确认 InnoDB 自动回滚了死锁事务,应用层做重试
  2. 定位热点SHOW ENGINE INNODB STATUS 找到死锁 SQL
  3. 优化方案
    • 加索引减少锁范围
    • 统一加锁顺序
    • 拆分大事务
    • 评估是否可以降到 RC
  4. 监控预警:监控 innodb_row_lock_waits 指标

相关链接