所有UPDATE/DELETE必须显式ORDER BY主键,跨表操作需约定全局锁序(如users→orders→order_items),慎用SELECT FOR UPDATE,批量操作须保证每批次内ID升序,INSERT...ON DUPLICATE KEY UPDATE更安全。

所有UPDATE/DELETE必须显式ORDER BY主键
不加ORDER BY的批量DML,InnoDB内部加锁顺序不可控——它可能按B+树遍历顺序、缓冲池热度甚至页分裂历史来决定,不同事务执行同一语句却产生不同加锁路径,死锁概率陡增。显式按主键升序是唯一能确保顺序一致的手段。
实操建议:
- 对
WHERE id IN (10, 5, 8)这类语句,必须写成WHERE id IN (10, 5, 8) ORDER BY id - 应用层拼接IN列表时,先对ID数组做升序排序再生成SQL,避免依赖数据库“自动整理”
- 不要用
LIMIT或OFFSET代替ORDER BY——没有确定性排序就等于放弃顺序控制
跨表操作必须约定全局访问顺序
两个事务分别先更新orders再更新order_items,和先order_items再orders,哪怕操作的行完全不重叠,也会因锁资源申请顺序错位触发死锁。这不是数据冲突,是资源调度层面的循环等待。
实操建议:
- 在团队规范文档里明确写出:所有涉及
orders、order_items、users的事务,强制按users → orders → order_items顺序操作 - 代码中每个跨表更新逻辑前加注释,例如
// LOCK ORDER: users → orders → order_items - 禁止在同一个事务里混用不同顺序的调用链(如A接口走users→orders,B接口却走orders→users)
慎用SELECT FOR UPDATE,优先改用带条件的UPDATE
SELECT ... FOR UPDATE会提前加锁并持有到事务结束,而UPDATE ... WHERE id = ? AND status = 'ready'只在真正修改时加锁,且失败即退出——既减少锁持有时间,又规避了“查完不改却一直占着锁”的陷阱。
实操建议:
- 如果业务目标只是“把某条记录改成processing”,直接用
UPDATE task SET status='processing' WHERE id=123 AND status='ready',检查ROW_COUNT()是否为1 - 必须用
SELECT FOR UPDATE时,确保WHERE条件命中唯一索引,避免间隙锁扩散;范围查询如WHERE created_at > '2026-08-01'极易引发隐式大范围锁 - 禁止在
SELECT FOR UPDATE后做耗时操作(如HTTP调用、文件读写),否则锁被捂在手里越久,死锁风险越高
批量操作拆分时仍要保证单批次内顺序一致
把1000行拆成10批处理,每批100行,但如果每批内部的ID顺序不固定(比如分页查出后没排序就直接更新),那10个事务之间依然可能形成错乱加锁链——死锁不是只发生在单个长事务里。
实操建议:
- 分页查询用于后续更新时,必须带
ORDER BY id,且分页参数基于上一批最大ID推进(WHERE id > ? ORDER BY id LIMIT 100),而非OFFSET - 用
INSERT ... ON DUPLICATE KEY UPDATE替代循环INSERT或UPDATE,让InnoDB在单条语句内完成排序与加锁 - 监控慢日志里
UPDATE执行时间超过100ms的语句,它们往往是批量逻辑未拆分或顺序失控的信号
最易被忽略的一点:加锁顺序一致性必须贯穿整个调用链,包括微服务间RPC。一个订单服务调用用户服务扣款,用户服务内部更新users表时,若其事务顺序和订单服务本地更新orders表的顺序不一致,死锁照常发生——锁的边界从来不止于单个数据库连接。


















