按主键升序更新可防死锁,因强制统一加锁顺序消除循环等待;需显式SELECT ORDER BY id取ID并代码排序,避免ORM或驱动重排;UUID主键慎用批量IN,建议改自增键或拆单条。

必须在应用层按主键升序组织ID,原生UPDATE不支持ORDER BY排序,靠SQL本身无法控制加锁顺序。
UPDATE语句里不能用ORDER BY强制排序
MySQL 8.0.19+虽在极少数单表场景下允许UPDATE ... ORDER BY,但不适用于带JOIN、子查询或批量IN的常见更新模式。InnoDB加锁顺序严格依赖扫描顺序,而扫描顺序由执行计划决定——它可能因索引选择、统计信息变化甚至查询缓存状态而波动。
- 别信“我加了索引,EXPLAIN显示走了主键,所以肯定有序”:即使走聚簇索引,若优化器选错范围扫描起点(比如用
WHERE status = ?触发全索引扫描),实际加锁仍可能乱序 - 用
SELECT id FROM t WHERE ... ORDER BY id显式取ID是唯一可靠方式,且必须把结果在代码里再做一次.sort()——避免ORM自动重排或网络传输导致顺序错乱 - 拼
UPDATE ... WHERE id IN (1,3,2)时,部分驱动(如PyMySQL旧版、某些Go driver)会主动对IN列表去重/重排,需实测验证输出SQL
按主键升序更新为什么能防死锁
死锁本质是循环等待:事务A持id=1锁等id=2,事务B持id=2锁等id=1。按主键升序更新相当于让所有事务“排队走同一条路”,物理上消除了交叉加锁路径。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 聚簇索引的主键顺序即数据物理存储顺序,InnoDB行锁+间隙锁的加锁行为天然与该顺序强绑定
- 对复合主键(如
(user_id, created_at))也有效,但排序必须用ORDER BY user_id, created_at,不能只按created_at——业务时间字段无聚簇意义 - UUID主键要特别小心:其随机性导致物理分散,即使排序后
IN列表有序,磁盘I/O跳跃仍可能引发锁竞争,此时更推荐拆成单条UPDATE或换自增代理键
IN子句顺序和批量拆分的取舍
批量UPDATE ... WHERE id IN (...)看似高效,但实际是双刃剑:它把多行锁压缩进一个事务,一旦其中某行被其他事务占用,整个语句就卡在等待上,放大阻塞面。
- 当待更新ID数≤200时,直接拆成单条
UPDATE按序执行更稳——每条语句持有锁时间极短,冲突窗口小 - 当ID数>200时,用
IN批量更新,但必须确保IN内ID严格升序,且事务内不做其他非幂等操作(如调外部API) - 严禁混合使用:比如先跑
UPDATE ... WHERE id IN (1,2,3),再跑UPDATE ... WHERE id = 4——这两者锁顺序不一致,照样死锁
容易被忽略的隐式陷阱
最危险的不是没排序,而是“以为排了但其实没生效”。几个高频漏点:
-
SELECT ... FOR UPDATE也必须按主键排序:如果先SELECT ... FOR UPDATE ORDER BY id拿到ID,但后续UPDATE没按这组ID顺序执行,等于白排 - ORM框架(如Django ORM、MyBatis)常默认对
IN参数去重或重排,需查文档确认行为,必要时绕过ORM手写SQL - 离线任务和实时任务写同一张表时,哪怕都按主键排序,若两边batch内ID集合有交集且排序逻辑不统一(比如一方用
ORDER BY id ASC,另一方用ORDER BY id DESC),依然死锁










