乱序更新必然触发死锁,因innodb死锁检测机制具有确定性:只要两个事务以不同顺序加锁同一组行,立即报错“deadlock found”,而非概率事件;加锁顺序由执行计划决定,非sql书写顺序,且仅在走主键/唯一索引时才按值排序加锁,否则全表扫描导致顺序失控。

乱序更新为什么必然触发死锁
不是“可能”死锁,而是只要两个事务以不同顺序更新同一组行,InnoDB 就会在加锁过程中立即检测到循环等待并报错 Deadlock found when trying to get lock。这不是概率问题,是 InnoDB 死锁检测机制的确定性行为。
UPDATE 加锁顺序由执行计划决定,不是 SQL 书写顺序
很多人以为写 UPDATE ... WHERE id IN (5,1,3) 就会按 5→1→3 的顺序加锁,其实不会。InnoDB 对 IN 列表会自动排序后加锁(实际按 1→3→5),但这个“可控排序”只适用于等值查询且走主键/唯一索引的情况。一旦 WHERE 条件没索引、或走的是非唯一二级索引,优化器就可能放弃有序扫描,转为全表扫描或 filesort,加锁顺序彻底失控。
- 有主键索引时:
WHERE id IN (5,1,3)→ 实际按id ASC顺序加锁(可预测) - 无索引或走二级索引时:
WHERE status = 'pending'→ 可能全表扫描,加锁顺序取决于数据物理分布和缓冲池状态(不可预测) - 复合索引未满足最左前缀:
INDEX (a,b),但只用WHERE b = 10→ 索引失效,退化为全表扫描
多事务交叉加锁的典型现场
假设表 t 有三行:id=1、id=2、id=3,且都有索引支持。
事务 A 执行:UPDATE t SET x=1 WHERE id IN (1,2) ORDER BY id DESC → 先锁 id=2,再锁 id=1
事务 B 同时执行:UPDATE t SET x=2 WHERE id IN (1,2) ORDER BY id ASC → 先锁 id=1,再锁 id=2
这时 InnoDB 在事务 A 想锁 id=1、事务 B 想锁 id=2 的瞬间,就判定为死锁——根本不需要等到第二行更新完成。
- ORDER BY 不等于加锁顺序保障,必须配合索引 + EXPLAIN 验证
- 哪怕只更新两行,只要顺序相反,死锁就成立
- 不靠运气,靠执行计划里
key字段是否命中、Extra是否含Using filesort
为什么游标式更新比 LIMIT 更可靠
LIMIT 在 UPDATE 中不提供稳定分片语义。比如你跑两次 UPDATE ... WHERE status='pending' ORDER BY id LIMIT 100,第二次可能跳过某些行(因为其他事务已更新它们),也可能重复处理(如果新插入的行 id 落在前一批范围内)。业务若依赖“严格分批”,就会漏或重。
游标式更新把顺序控制权交还给应用层:
- 第一次查出最大 id:
SELECT id FROM t WHERE status='pending' ORDER BY id LIMIT 100 - 记录最后一条的
id = 105 - 下一批用:
UPDATE t SET status='processing' WHERE id > 105 AND status='pending' ORDER BY id LIMIT 100
这种写法天然保证单调递增、无重无漏,且每次只锁一个连续区间,间隙锁范围更小,冲突面更窄。
真正容易被忽略的点:死锁日志里显示的“被回滚事务”,往往不是写得最复杂的那个,而是恰好撞上加锁路径交叉的那个。排查时别只盯 SQL 写法,先看 SHOW ENGINE INNODB STATUS 里锁定的具体索引项和 gap 范围——那才是加锁顺序失控的证据。











