死锁是加锁顺序冲突的必然结果,根本原因是多个事务以不一致顺序争抢同一组锁资源形成循环等待;InnoDB检测到闭环后回滚undo量小的事务。

死锁不是偶然,是加锁顺序冲突的必然结果
MySQL事务发生死锁,根本原因不是“并发太高”或“数据库太忙”,而是多个事务以**不一致顺序争抢同一组锁资源**,形成循环等待。InnoDB 检测到这种闭环后,必须回滚一个事务(选 undo 量小的那个),否则所有相关事务将永远卡住。
最常见场景:多表更新顺序不一致
两个事务操作完全相同的表和行,但加锁顺序相反——这是生产环境里超 60% 死锁的直接原因。
- 事务 A 执行:
UPDATE user SET balance = balance - 100 WHERE id = 1→ 再执行UPDATE order SET status = 'paid' WHERE user_id = 1 - 事务 B 同时执行:
UPDATE order SET status = 'shipped' WHERE user_id = 1→ 再执行UPDATE user SET updated_at = NOW() WHERE id = 1
只要并发稍高,A 持有 user.id=1 等 order.user_id=1,B 持有 order.user_id=1 等 user.id=1,死锁立刻触发。
容易被忽略的隐性死锁源:索引缺失 + 全表扫描
当 WHERE 条件没走索引,InnoDB 会逐行扫描聚簇索引并加行锁——相当于给整张表打上大量 X 锁。此时哪怕只是两条简单 UPDATE,也可能因锁住不同行却互相等待而死锁。
- 典型错误现象:
EXPLAIN显示type = ALL或key = NULL -
SHOW ENGINE INNODB STATUS中看到锁类型为X,但lock_trx_id下锁定行数成百上千 - 慢查询日志里
Rows_examined远大于实际匹配行数
比如 UPDATE product SET stock = stock - 1 WHERE category_id = 5 AND status = 'on_sale',若没建 (category_id, status) 联合索引,就极易引发这类“伪随机”死锁。
间隙锁(Gap Lock)在 RR 隔离级下的意外卷入
REPEATABLE READ 是 InnoDB 默认隔离级别,它启用临键锁(Next-Key Lock = Record Lock + Gap Lock)。这意味着即使你更新的是不存在的记录,也可能锁住一段“间隙”,阻塞并发插入——而这个行为常被开发者完全忽略。
- 例如:
SELECT * FROM user WHERE age BETWEEN 25 AND 30 FOR UPDATE,会锁住 (25,30) 这个区间,别人插age=27就会被阻塞 - 更隐蔽的是:
DELETE FROM user WHERE id = 999(id=999 不存在),InnoDB 仍可能加 Gap Lock 锁住 (max_id, +∞),导致后续INSERT卡住 - 这种锁不显式对应某行,但在
SHOW ENGINE INNODB STATUS的*** (1) WAITING FOR THIS LOCK TO BE GRANTED:段里能看到gap before rec字样
间隙锁本身不直接导致死锁,但它扩大了锁范围、增加了锁冲突概率,尤其在批量插入/删除+范围查询混用时,很容易成为死锁导火索。


















