死锁主因是事务对同一组行加锁顺序不一致,而非并发量高;须用EXPLAIN验证索引命中与排序生效,避免无索引ORDER BY、非主键索引更新及间隙锁陷阱。

死锁不是并发高导致的,而是多个事务以不同顺序锁定同一组行时立刻触发的——哪怕只有两个事务、每秒只跑几条 UPDATE,只要加锁顺序不一致,就可能秒级死锁。
UPDATE 多行时加锁顺序不一致是主因
MySQL InnoDB 对 UPDATE 加锁不是原子动作,而是逐行扫描 + 条件匹配 + 行锁升级的过程。当两个事务对同一组记录(比如 id=1 和 id=5)以相反顺序加锁时,循环等待即刻形成:
- 事务A 执行
UPDATE t SET x=1 WHERE id IN (1,5)→ 按索引顺序先锁id=1,再锁id=5 - 事务B 同时执行
UPDATE t SET x=2 WHERE id IN (5,1)→ 若优化器走不同执行路径,可能先锁id=5,再锁id=1 - 结果:A 持有
id=1等id=5,B 持有id=5等id=1→ InnoDB 检测到死锁,回滚其中一个
验证方式:对语句跑 EXPLAIN FORMAT=TRADITIONAL,确认 key 显示命中索引、Extra 不含 Using filesort。
ORDER BY 不等于安全,必须验证是否真走索引
很多人以为加个 ORDER BY id 就能统一加锁顺序,但实际是否起作用,完全取决于执行计划是否真的用索引完成排序:
- 如果
WHERE status = 'pending'字段没索引,即使写了ORDER BY id,MySQL 也可能全表扫描后再filesort→ 锁住所有匹配行,且加锁顺序不可控 - 复合索引要注意最左前缀:
(user_id, created_at)索引下,WHERE user_id = 123 ORDER BY created_at能走索引;但WHERE status = 'x' ORDER BY created_at就无效
真正起效的写法是子查询强制排序:UPDATE t SET x=1 WHERE id IN (SELECT id FROM (SELECT id FROM t WHERE ... ORDER BY id) AS tmp),或应用层先排序再拼 IN 列表。
SELECT FOR UPDATE 在无记录时也会死锁
这是最容易被忽略的陷阱:RR 隔离级别下,SELECT ... FOR UPDATE 即使查不到记录,也会加间隙锁(Gap Lock),多个并发请求会争抢同一间隙,再叠加后续 INSERT 的插入意向锁,直接构成死锁链:
- 错误写法:
SELECT * FROM orders WHERE order_no = 'ABC' FOR UPDATE→ 记录不存在 → 所有并发请求都卡在同一个间隙上 - 正确替代:
INSERT INTO orders (order_no, user_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE updated_at = NOW(),前提是order_no有UNIQUE约束 - 若必须用
SELECT FOR UPDATE,确保WHERE条件字段有唯一索引,且EXPLAIN显示type = const或ref、rows = 1
非主键索引更新会引发双重加锁冲突
当 WHERE 条件使用非主键索引时,MySQL 会先锁非主键索引项,再回表锁主键索引 —— 这个分步加锁过程本身就容易被其他事务“插队”:
- 事务A 用
WHERE idx_col = X更新 → 先锁idx_col,再锁主键 - 事务B 用
WHERE id = Y(主键)更新 → 先锁主键,再锁idx_col - 二者在中间步骤交叉持锁,立即形成死锁
解决办法很直接:批量更新尽量走主键,或改用主键列表驱动:UPDATE t SET ... WHERE id IN (1,2,3) ORDER BY id;避免混用不同索引条件更新同一张表。
真正难处理的不是“怎么重试”,而是“怎么让第一次就不死锁”——加锁顺序、索引有效性、语句执行路径这三者必须全部对齐,缺一不可。线上环境一旦出现 Deadlock found when trying to get lock,优先检查 EXPLAIN 输出和索引定义,而不是调大重试次数。










