MySQL死锁是事务并发加锁顺序冲突的必然结果,InnoDB自动回滚一事务并报错;根本在于锁范围失控(如无索引导致全表扫描)、加锁顺序不一致(如IN列表或二级索引回表)、间隙锁干扰等,须通过EXPLAIN验证执行计划、统一加锁顺序、确保索引命中来收敛行为。

MySQL执行UPDATE语句时死锁不是配置错误,而是事务并发加锁顺序冲突的必然结果;InnoDB会自动回滚一个事务并报Deadlock found when trying to get lock,但业务不能只靠重试扛住——关键得让加锁行为可预测、可收敛。
UPDATE WHERE没走索引,锁范围失控
WHERE条件无索引时,InnoDB退化为全表扫描,每行都加记录锁(X),还可能触发filesort导致加锁顺序完全不可控。多个并发UPDATE扫同一张表,极易形成交叉等待。
- 用
EXPLAIN FORMAT=TRADITIONAL验证实际执行计划:重点看key是否命中预期索引、Extra是否含Using filesort或Using where; Using index condition - 复合索引要注意最左前缀:比如有
(status, id)索引,WHERE status = 'pending' ORDER BY id才有效;若只写WHERE id > 100,该索引基本失效 - 避免在
WHERE中混用函数或类型隐式转换,如WHERE DATE(create_time) = '2026-09-01'会跳过索引
批量UPDATE加锁顺序不一致
UPDATE ... WHERE id IN (2,1)和UPDATE ... WHERE id IN (1,2)在InnoDB中加锁顺序未必与传入顺序一致——它取决于索引B+树遍历路径,而非SQL字面顺序。
- 改用游标式更新:
UPDATE t SET status='processing' WHERE id > ? AND status='pending' ORDER BY id LIMIT 100,每次记录上一批最大id值作为下一批起点 - 禁用
LIMIT做分页控制:它不保证幂等性,数据变动时可能漏行或重复处理 - 避免
WHERE status IN ('a','b')这类非确定性条件与LIMIT混用,不同事务扫描到的实际行集可能不同
SELECT FOR UPDATE查不到记录也死锁
在RR隔离级别下,SELECT * FROM orders WHERE order_no = 'ABC' FOR UPDATE即使没查到任何记录,也会对order_no所在索引间隙加Gap Lock。多个并发请求争抢同一间隙,再叠加后续INSERT的插入意向锁,立刻构成死锁链。
- 优先用
INSERT INTO ... ON DUPLICATE KEY UPDATE替代,前提是order_no有UNIQUE约束 - 若必须用
SELECT FOR UPDATE,确保WHERE字段有唯一索引,且EXPLAIN显示type = const或ref、rows = 1 - 绝对避免对高重复值字段(如
name)做SELECT FOR UPDATE,间隙锁冲突概率陡增
非主键索引更新引发两阶段加锁
当WHERE走二级索引(如KEY idx_status (status))时,InnoDB先锁二级索引项,再回表锁主键。这中间存在时间窗口:事务A刚锁完二级索引、还没锁主键时,事务B可能已锁住对应主键——反过来也成立,形成典型死锁。
- 尽量让
UPDATE走主键或唯一索引,避免二级索引回表路径 - 如果必须走二级索引,考虑在事务开头显式
SELECT id FROM t WHERE status = ? LOCK IN SHARE MODE提前锁定主键,再执行UPDATE - 注意联合索引中非前导列无法生效:索引是
(a,b),但WHERE b = 1不会使用该索引,仍可能全表扫描
真正难处理的从来不是“怎么加锁”,而是“加锁边界是否稳定”——间隙锁范围、索引是否命中、执行计划是否漂移,这些细节一旦松动,死锁就从偶发变成高频。别信ORDER BY能保顺序,也别赌LIMIT能分片,所有看似安全的语法,都得用EXPLAIN和SHOW ENGINE INNODB STATUS亲手验证。


















