批量更新前必须按主键ID升序排序以避免死锁,因无序加锁易导致循环等待;单次IN长度≤100,优先用BETWEEN或分批处理,并确保索引命中与隔离级别适配。

批量更新前必须排序主键ID
死锁在批量更新中最常见的诱因,就是多个事务对同一组记录以不同顺序加锁。比如事务A按 id IN (10, 5, 8) 更新,事务B按 id IN (8, 10, 5) 更新,InnoDB会按各自SQL中出现的顺序逐行加X锁,极易形成循环等待。
解决方法不是靠“运气”或“重试”,而是强制统一物理加锁顺序——只有一种可靠方式:按主键升序排列后再执行更新。
- 应用层做排序:拿到待更新ID列表后,先
Arrays.sort()或sorted(),再拼进IN语句或分批处理 - 避免用ORM自动生成的无序ID列表(如MyBatis
<foreach></foreach>默认不保序) - 若用存储过程,改用临时表+
ORDER BY id游标,而非直接遍历输入数组
别用IN一次性更新大量ID
UPDATE t SET status=1 WHERE id IN (1,2,3,...,5000) 看似简洁,但实际会触发大量行锁,且InnoDB内部加锁顺序仍可能受执行计划影响;更危险的是,若该语句没走索引,还会退化成表锁。
批量操作必须控制单次锁粒度:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 单次IN列表长度建议 ≤ 100,上限不超过500(视平均行大小和buffer pool而定)
- 使用
WHERE id BETWEEN ? AND ?配合有序主键范围,比乱序IN更可控 - 真要更新上万行?拆成事务内多轮小批量,每轮提交后释放锁,避免长事务堆积
警惕隐式锁升级和间隙锁干扰
即使你严格排序了ID,如果更新语句没命中索引,或者隔离级别是RR且条件涉及范围,InnoDB仍可能加间隙锁(Gap Lock)或临键锁(Next-Key Lock),把“不该锁的空隙”也锁住,扩大冲突面。
验证和规避要点:
- 执行前必看
EXPLAIN:确认type是const或range,绝不能是ALL或index - RR级别下,避免
WHERE status = 'pending'这类非唯一条件批量更新——它会锁住所有匹配行+间隙 - 若业务允许,临时降级到RC隔离级别(需全局评估一致性影响),可消除间隙锁
应用层加锁顺序必须与数据库一致
很多团队只管SQL排序,却忽略了应用逻辑里的“软顺序”。例如:一个转账服务先查account_a再查account_b,另一个风控服务反着来,哪怕两条UPDATE都按ID排序,中间SELECT FOR UPDATE的顺序不一致,照样死锁。
真正落地时,锁顺序要贯穿整个事务链:
- 所有涉及多记录的业务操作,定义全局锁序规则(如“永远先锁user_id小的账户”)
- DAO层封装
lockAccountsByIds(List<long> ids)</long>方法,内部自动排序并统一调用 - 禁止在同一个事务里混用不同粒度的锁:比如先
SELECT ... FOR UPDATE查两行,再UPDATE ... WHERE id IN (...)更新同一组——这两者加锁路径可能不重合

















