MySQL INSERT死锁主因是主键与插入顺序不匹配,而非并发过高;需主键单调递增且插入严格保序,配合应用层排序、分桶等策略降低间隙锁冲突。

死锁不是并发太高,是主键和插入顺序共同触发的
MySQL 的 INSERT 死锁在高并发写入时高频出现,但根源往往不在并发量本身,而在主键设计(尤其是自增 ID)与业务插入顺序不匹配。InnoDB 在插入时会对间隙(gap)加锁,如果多个事务按不同顺序插入相邻或重叠的主键区间,就极易形成循环等待。
常见现象:Deadlock found when trying to get lock; try restarting transaction,且 SHOW ENGINE INNODB STATUS 显示两个事务都在等对方持有的 INSERT intention lock 或 gap lock。
- 使用 UUID 或雪花 ID 作为主键时,若未排序直接批量插入,会大幅加剧间隙锁冲突
- 按时间戳前缀分表 + 自增 ID 的组合,若插入时间乱序(如延迟消息、重试请求),会导致跨分表/跨页锁竞争
-
INSERT ... ON DUPLICATE KEY UPDATE比纯INSERT更容易死锁,因为它需要先查再锁,扩大了锁范围
主键必须单调递增,且插入必须严格保序
这不是“建议”,而是 InnoDB 间隙锁机制下降低死锁概率最有效的硬约束。只要主键值整体趋势递增,且同一批插入按主键升序排列,InnoDB 就能尽量复用同一段间隙锁,避免交叉锁定。
实操建议:
- 优先用
BIGINT UNSIGNED AUTO_INCREMENT,禁用UUID()或MD5()直接作主键 - 如果必须用分布式 ID(如雪花算法),插入前务必对这批数据按 ID 升序
ORDER BY—— 不是靠数据库排序,是应用层排序后再拼INSERT - 批量插入时,拆成每 500 行一组,并确保每组内主键严格递增;不要把 1000 行乱序 ID 一次性塞进一条
INSERT ... VALUES (...), (...) - 避免在主键上建
UNIQUE索引以外的二级索引过多,否则每个索引都要加 gap lock,锁粒度翻倍
用 INSERT IGNORE 替代 ON DUPLICATE KEY UPDATE 可显著降死锁率
两者语义不同,但很多场景下 IGNORE 足够用,且它只在发生唯一键冲突时静默跳过,不触发行级更新锁。而 ON DUPLICATE KEY UPDATE 无论是否真更新,都会对目标行加 X 锁,还可能触发二级索引维护锁。
适用场景:
- 幂等写入(如日志、事件记录),失败可丢弃,无需更新旧值
- 主键冲突即代表“已存在”,无需回查或修改字段
- 写入链路中有上游去重逻辑,DB 层只需防重复落库
注意:INSERT IGNORE 遇到非主键/唯一键冲突(如外键、检查约束)会报错,不是全静默。
插入前加一层轻量级协调,比调优更治本
当业务无法控制插入顺序(比如多服务、多实例同时写同一张表),硬靠 DB 参数调优(如调小 innodb_lock_wait_timeout)只是掩盖问题。真正低成本的解法,是在应用层做最小干预:对关键字段哈希后取模,让相同业务实体(如 user_id)始终由同一个线程/连接插入。
例如:
INSERT INTO t_order (id, user_id, amount) VALUES (?, ?, ?) -- 应用层按 user_id % 4 分桶,每个桶配独立连接池或串行队列
这样既不改变主键结构,也不依赖 MySQL 配置,还能自然实现锁隔离。复杂点在于分桶策略要和业务主键强相关,且模数不能太小(否则热点集中),也不能太大(管理成本高)。
最容易被忽略的是:这种协调必须作用于「事务边界内」——不能一个事务里混插不同 user_id 的数据,否则锁还是会跨桶打起来。


















