死锁不是数据库故障,而是高并发下事务加锁顺序冲突的必然结果;需立即执行show engine innodb status\g查看latest detected deadlock段,聚焦两事务的sql、holds the lock(s)和waiting for this lock to be granted,并结合innodb_print_all_deadlocks持久化日志与information_schema.innodb_lock_waits实时监控定位根因。

死锁不是数据库故障,而是高并发下事务加锁顺序冲突的必然结果。只要应用中存在多资源操作且顺序不一致,就一定会发生——关键不是“会不会”,而是“怎么快速定位并稳定复现”。
怎么看最近一次死锁详情(SHOW ENGINE INNODB STATUS\G)
这是排查死锁的第一步,也是唯一能立刻拿到现场快照的方式。执行后重点盯住 LATEST DETECTED DEADLOCK 区域:
- 它只保留最近一次死锁记录,所以必须在报错后立即查,不能等第二天
- 里面会明确写出两个事务各自的
SQL、持有的锁(HOLDS THE LOCK(S))和等待的锁(WAITING FOR THIS LOCK TO BE GRANTED) - 注意看锁类型:比如
lock_mode X locks rec but not gap表示是精确行锁,而lock_mode X locks gap before rec就是间隙锁,后者在范围查询中极易引发交叉等待 - 如果事务里有
WHERE product_id = ? AND quantity > 0这类条件,但quantity列没索引,InnoDB 可能全表扫描并锁住大量行——日志里会显示n bits 80(锁位图很大),这就是锁扩大的信号
怎么查当前正在阻塞的事务(information_schema.innodb_lock_waits)
当死锁正在发生、但还没被 InnoDB 自动检测到时(极少见),或你想确认某个慢查询是否卡在锁上,这个视图就是实时诊断工具:
- 执行
SELECT * FROM information_schema.innodb_lock_waits;能直接看到谁在等谁、线程 ID 是多少 - 结果中的
blocking_thread对应information_schema.innodb_trx.trx_mysql_thread_id,可直接用于KILL操作 - 注意:MySQL 8.0+ 才支持该视图;5.7 需用
performance_schema.data_locks+data_lock_waits替代,字段名和关联方式完全不同 - 别依赖这个视图“预防”死锁——它只反映当前等待,不反映循环依赖关系;死锁检测是 InnoDB 内部图算法完成的,外部不可见
怎么让每次死锁都留痕(innodb_print_all_deadlocks = ON)
线上环境绝不能只靠 SHOW ENGINE INNODB STATUS 碰运气。必须开启全量死锁日志,否则错过一次就等于失去根因证据:
- 临时开启:
SET GLOBAL innodb_print_all_deadlocks = ON;(重启失效) - 永久生效:在
my.cnf的[mysqld]段落加一行innodb_print_all_deadlocks = 1 - 日志默认输出到 MySQL 错误日志(
log_error配置路径),不是 slow log,也不是 general log - 每条死锁记录以
------------------------ LATEST DETECTED DEADLOCK ------------------------分隔,grep 很方便;但注意它不带时间戳前缀,需结合日志文件本身的行时间判断 - 开启后日志体积增长明显,但比起丢掉一次生产事故的根因,这点磁盘空间值得花
为什么加了索引还是死锁(gap lock 和 next-key lock)
很多开发者以为“加了主键/唯一索引就万事大吉”,结果在 REPEATABLE READ 隔离级别下,范围查询仍会触发间隙锁,导致看似无关的插入/更新互相阻塞:
- 例如
UPDATE product_stock SET quantity = quantity - 1 WHERE product_id > 1000 AND status = 'on',即使product_id有索引,也会锁住(1000, +∞)这个间隙 - 另一个事务执行
INSERT INTO product_stock (product_id, ...) VALUES (1001, ...)就会被卡住——因为插入意向锁要等间隙锁释放 - 解决方案不是删索引,而是:① 改用
READ COMMITTED隔离级别(间隙锁失效);② 把范围查询改成等值查询(如先SELECT id FROM ... WHERE ...拿到主键再UPDATE ... WHERE id IN (...));③ 在业务层加分布式锁控制热点数据并发 - 特别注意:MyBatis 的
<foreach></foreach>动态 SQL 如果生成了IN列表,但列表为空,可能退化成全表扫描——这种隐式行为最容易漏查
真正难的不是看懂死锁日志,而是把日志里的锁模式、索引名、SQL 条件和业务逻辑串起来——比如发现两个事务都在等 idx_order_id 上的 X 锁,就得立刻反查代码:是不是支付回调和取消订单用了不同事务入口?是不是一个走 MyBatis XML、一个走注解,导致 where 条件拼接顺序不一致?这些细节,日志不会告诉你,只能靠人去对。











