死锁是innodb即时检测到的循环等待并自动回滚事务,而innodb_lock_wait_timeout仅为超时机制;查死锁须用show engine innodb status看最后一次详细日志,重点关注两个事务的锁等待与持有关系。

死锁不是超时,innodb_lock_wait_timeout 不会触发死锁检测
很多人看到 Lock wait timeout exceeded 就以为是死锁,其实这是两回事。MySQL 的死锁检测是即时的、自动的,一旦发现循环等待,立刻选一个事务回滚并报错 Deadlock found when trying to get lock;而 innodb_lock_wait_timeout 是纯超时机制——某个事务等锁太久(默认 50 秒),直接被干掉,不涉及死锁判断。
所以:查死锁不能只盯 innodb_lock_wait_timeout 的值,更不能靠调大它来“避免死锁”。它只是掩盖问题的创可贴。
怎么看真正的死锁日志:用 SHOW ENGINE INNODB STATUS
MySQL 每次发生死锁,都会把最近一次的详细信息写进 InnoDB 状态里,SHOW ENGINE INNODB STATUS 是唯一靠谱的入口。注意它只保留最后一次,不是历史记录。
实操建议:
- 执行
SHOW ENGINE INNODB STATUS\G(加\G是为了格式化输出) - 重点找
*** (1) TRANSACTION:和*** (2) TRANSACTION:这两段,它们分别代表两个互相卡住的事务 - 看每段里的
mysql tables in use、locked tables,确认涉及哪些表和索引 - 关键看
WAITING FOR THIS LOCK TO BE GRANTED:和HOLDS THE LOCK(S):——谁在等什么锁、谁拿着什么锁,一目了然
为什么 SELECT ... FOR UPDATE 容易引发死锁
不是语句本身有问题,而是它依赖的索引路径和执行顺序不确定。InnoDB 加锁是基于索引的,如果两个事务以不同顺序访问同一组行(比如一个按 id 扫,一个按 name 查),就可能形成交叉加锁链。
常见诱因:
- 没有走索引的
SELECT ... FOR UPDATE→ 全表扫描 + 行锁升级为间隙锁,范围不可控 - 事务中先
UPDATE再SELECT ... FOR UPDATE,而另一个事务顺序相反 - 批量操作用
IN列表,但传入 ID 顺序不一致(如前端随机拼接) - 使用
ORDER BY但排序字段无索引,导致锁范围扩大
如何稳定复现和验证死锁逻辑
线上死锁偶发难抓?别靠猜。用两个 MySQL 客户端手动模拟是最有效的验证方式。
步骤要点:
- 两个会话都关掉自动提交:
SET autocommit = 0; - 会话 A 先更新某行:
UPDATE t SET x=1 WHERE id = 10; - 会话 B 紧接着更新另一行:
UPDATE t SET x=1 WHERE id = 20; - 再让会话 A 去更新 B 刚锁的行:
UPDATE t SET x=1 WHERE id = 20;(此时卡住) - 会话 B 同时去更新 A 的行:
UPDATE t SET x=1 WHERE id = 10;→ 瞬间报Deadlock found when trying to get lock
这个过程能清晰暴露锁等待关系。真实业务中,只要两个事务对同一组主键/索引键以不同顺序加锁,就具备死锁条件。
真正麻烦的是隐式锁升级和二级索引上的间隙锁——它们不会直接出现在 SQL 里,但会在 SHOW ENGINE INNODB STATUS 的 lock mode 描述中露出马脚,比如看到 lock_mode X locks gap before rec 就得小心了。











