死锁不是InnoDB“没处理好”,而是它主动检测并牺牲一个事务
InnoDB 遇到死锁时不会卡住或无限等待,而是通过内置的死锁检测器(wait-for graph)在几毫秒内识别出循环等待,并选择一个代价最小的事务回滚——这个被回滚的事务会收到
Deadlock found when trying to get lock; try restarting transaction错误。关键点在于:这不是故障,是正常策略。常见误解是“加了索引就不会死锁”,其实只要两个事务以不同顺序访问相同资源(哪怕都是走索引),就可能触发死锁。
典型死锁场景:UPDATE 顺序不一致 + 无覆盖索引
最常复现的死锁发生在多条件 UPDATE 或范围操作中,尤其当 WHERE 条件未命中唯一索引、或扫描路径不可预测时。例如:
-- 事务A UPDATE accounts SET balance = balance - 100 WHERE user_id = 1001 AND status = 'active'; -- 事务B UPDATE accounts SET balance = balance + 100 WHERE user_id = 1002 AND status = 'active';
如果
user_id是主键但
status没有索引,InnoDB 可能对整个二级索引
status进行范围扫描,锁住多个间隙(gap lock),而两个事务又交叉更新不同
user_id行,就容易形成锁等待环。 务必为 WHERE 中高频出现的非主键字段建立联合索引,例如
(status, user_id),让查询能走索引且扫描范围确定 避免在事务中执行不确定扫描范围的操作,如
SELECT ... FOR UPDATE带模糊条件(
LIKE '%abc') UPDATE 语句尽量只更新必要字段,减少锁升级概率(例如不要用
SET *)
如何快速定位死锁原因:看 SHOW ENGINE INNODB STATUS
执行该命令后,输出末尾的
LATEST DETECTED DEADLOCK区域才是关键。它包含: 两个事务各自持有的锁(
HELD LOCKS)和正在等待的锁(
WAITING FOR THIS LOCK TO BE GRANTED) 每个事务最后执行的 SQL(注意:不一定是导致死锁的那条,但通常是最近的 DML) 锁类型(
record lock、
gap lock、
next-key lock)和锁定的具体索引名
重点比对两事务的
lock_mode和
lock_trx_id,再结合表结构判断是否因索引缺失或访问顺序不一致导致。不要只看 SQL 文本,要看实际加锁的索引和记录位置。
死锁无法完全避免,但可大幅降低:重试 + 顺序约定 + 小事务
应用层必须处理
Deadlock found...错误,简单重试 1–2 次通常就能成功。除此之外: 所有涉及多行更新的业务逻辑,强制按主键升序(或固定字段排序)处理,例如先
SELECT ... ORDER BY id FOR UPDATE再逐条更新 拆分大事务:一个事务里不要同时操作用户表、订单表、库存表;把跨表逻辑放到应用层协调 监控慢日志中频繁出现的
UPDATE ... WHERE ...语句,检查其
EXPLAIN是否走了预期索引,有没有隐式类型转换导致索引失效
真正难调的死锁往往藏在看似简单的批量更新里——比如用
IN (1,5,3,9)更新,InnoDB 实际加锁顺序可能按索引物理位置而非 IN 列表顺序,这时候显式
ORDER BY就很关键。
