死锁可100%复现:事务a先锁id=1再等id=2,事务b先锁id=2再等id=1,形成互斥等待环;空表并发insert因争夺(-∞,+∞)间隙的插入意向锁也必现死锁。

直接模拟出可复现的死锁,关键不是“压测并发量”,而是精准构造两个事务的**加锁顺序冲突**——只要满足“事务A先锁a再等b,事务B先锁b再等a”,哪怕只有2个连接,100%触发死锁。
用 SHOW ENGINE INNODB STATUS 确认死锁已发生
这是验证闭环是否成立的第一步。死锁不是靠猜,而是靠日志说话:
- 执行完疑似死锁的并发操作后,立刻在 MySQL 客户端运行
SHOW ENGINE INNODB STATUS\G - 滚动到
------------------------ LATEST DETECTED DEADLOCK ------------------------区域 - 必须看到两段
(1) TRANSACTION和(2) TRANSACTION,且各自HOLDS THE LOCK(S)与WAITING FOR THIS LOCK TO BE GRANTED形成互指(A 持有 id=1 等 id=2,B 持有 id=2 等 id=1) - 如果只看到
LOCK WAIT但没出现LATEST DETECTED DEADLOCK,说明只是锁等待,还没形成死锁闭环
用两个终端手动构造经典转账死锁
避免脚本封装带来的时序干扰,手敲最可控。假设表 account 有主键 id,数据含 id=1 和 id=2:
- 终端 A:
BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1;(此时持有 id=1 的 X 锁) - 终端 B:
BEGIN; UPDATE account SET balance = balance - 50 WHERE id = 2;(此时持有 id=2 的 X 锁) - 终端 A 继续:
UPDATE account SET balance = balance + 100 WHERE id = 2;(开始等待 id=2) - 终端 B 继续:
UPDATE account SET balance = balance + 50 WHERE id = 1;(开始等待 id=1)
3–5 秒内,其中一个会报错:Deadlock found when trying to get lock; try restarting transaction —— 闭环完成。
空表插入必现间隙锁死锁(RR 隔离级下)
这个场景容易被忽略,但线上高频:空表 + 并发 INSERT 就会因插入意向锁竞争触发死锁,不依赖业务逻辑:
- 确保隔离级别为
REPEATABLE READ(默认):SELECT @@transaction_isolation; - 清空表:
TRUNCATE TABLE t;(或新建空表) - 开两个连接,同时执行:
INSERT INTO t(id, name) VALUES (1, 'a'); - 必有一条报
Deadlock found when trying to get lock;另一条成功,且自增 ID 跳号(如成功插入 id=1 和 id=3) - 原因:空表时整个主键范围是单个间隙
(-∞, +∞),两个事务都申请该间隙的插入意向锁,在锁排队阶段形成循环等待
关闭死锁检测后只能靠超时被动退出
想验证“无检测机制下死锁如何表现”,需临时关掉 innodb_deadlock_detect:
- 执行:
SET GLOBAL innodb_deadlock_detect = OFF;(需 SUPER 权限) - 再跑上面任一死锁案例,你会发现:两个事务都不报错,但都卡住不动
- 直到
innodb_lock_wait_timeout(默认 30 秒)触发,其中一个报错:Lock wait timeout exceeded; try restarting transaction - 注意:关检测只用于测试,生产严禁长期关闭——它会让问题从秒级暴露变成数十秒阻塞,更伤业务
真正难的不是复现死锁,而是复现后能从 SHOW ENGINE INNODB STATUS 日志里快速定位哪两条 SQL、哪个索引、哪种锁类型在打架。间隙锁和插入意向锁的交互尤其隐蔽,日志里 lock_mode X locks gap before rec 这类描述必须盯住看。











