InnoDB对主键/唯一索引的IN查询自动按主键升序重排加锁,故SQL中IN顺序无效;但普通索引下不重排,应用层必须显式排序以统一加锁路径防死锁。

为什么IN (5,2,3) 里的 ID 顺序不决定加锁顺序
很多人以为把 UPDATE t SET x=1 WHERE id IN (5,2,3) 写成 IN (2,3,5) 就能“控制顺序”,其实这是误解。InnoDB 对主键或唯一索引的 IN 查询,内部会自动按主键值升序重排后再扫描加锁——无论你写的顺序如何。真正起作用的是“最终扫描路径”,不是 SQL 文本顺序。
但这个自动重排只在命中主键/唯一索引时生效;如果 WHERE 走的是普通索引(比如 INDEX(status)),InnoDB 不做重排,此时应用层不排序,不同事务可能以任意顺序加锁,死锁风险陡增。
- 验证方式:对语句执行
EXPLAIN FORMAT=TRADITIONAL,看key列是否命中主键,Extra是否含Using filesort(有则说明没走索引或排序失控) - 复合索引场景下,若
WHERE status = ? ORDER BY id,必须确保status在联合索引最左,否则ORDER BY无法复用索引,仍可能触发 filesort
批量更新不按主键排序的真实死锁链路
假设两个事务并发执行:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
事务 A:UPDATE order SET status='paid' WHERE id IN (100, 50);
事务 B:UPDATE order SET status='shipped' WHERE id IN (50, 100);
表面看都是同一批 ID,但若表上只有普通索引(如 INDEX(status)),InnoDB 可能按索引叶子节点物理顺序扫描,而不同事务因缓存、统计信息差异,扫描路径不一致——A 先锁 50 再锁 100,B 反过来,立刻形成循环等待。
- 即使走主键索引,若应用层分页查 ID 后拼
IN,但两次查询没加ORDER BY id,返回顺序可能不同,导致后续IN列表顺序漂移 - ORM(如 MyBatis)动态拼接 WHERE 条件时,字段顺序不固定,也可能让优化器选择不同执行路径
按主键升序更新为什么能打破循环等待
核心不是“排序动作”本身,而是它强制所有事务对同一组记录采用完全相同的加锁路径。只要所有代码路径都先锁 id=50、再锁 id=100,就不会出现 A 等 B、B 等 A 的闭环。
- 应用层显式排序是最可控的方式:
SELECT id FROM order WHERE ... ORDER BY id→ 得到有序列表 →UPDATE ... WHERE id IN (?, ?, ?) - 更稳妥的做法是游标式分批:
UPDATE t SET status='processing' WHERE id > ? AND status='pending' ORDER BY id LIMIT 100,每次用上一批最大id作为下一批起点 - 极端一致性要求下,直接逐条按序执行:
UPDATE t SET x=1 WHERE id = 50;→UPDATE t SET x=1 WHERE id = 100;,牺牲吞吐换确定性
ORDER BY 在 UPDATE 中不是银弹
UPDATE ... ORDER BY id 看似简洁,但它不保证加锁安全——前提是 WHERE 必须走索引。如果 WHERE 条件无索引,MySQL 会全表扫描 + filesort,这时 ORDER BY 只是最后排序结果,加锁仍发生在扫描过程中,顺序不可控,甚至锁住大量无关行。
- 典型错误:
UPDATE t SET x=1 WHERE name LIKE '%abc%' ORDER BY id——LIKE前缀模糊导致索引失效,ORDER BY彻底失效 - 正确姿势:先用
EXPLAIN确认key非NULL、rows值合理、Extra不含Using filesort - RR 隔离级别下,
SELECT ... FOR UPDATE即使没查到数据也会加间隙锁,若没主键约束且并发查相同条件,多个事务争抢同一间隙,叠加后续 INSERT 就构成死锁链

















