主键冲突时卡住是因为insert ... on duplicate key update会先加x锁和插入意向锁,导致并发插入相同唯一键时事务阻塞等待;应优先用insert ignore,批量插入须升序且分批,分布式id需严格保序。

主键冲突时为什么不是报错而是卡住?
因为用了 INSERT ... ON DUPLICATE KEY UPDATE,它在检测唯一键冲突前,会先对目标行(或间隙)加 X lock 和 insert intention lock。如果多个事务同时尝试插入相同唯一键(比如订单号、手机号),第一个事务持锁,其余全部阻塞等待——不是死锁,但会触发 Lock wait timeout exceeded。
常见于:应用层生成 ID 未做去重(如本地时间戳+随机数)、消息重试未幂等、分库分表后全局 ID 生成器漂移。
- 查
SHOW ENGINE INNODB STATUS,重点看TRANSACTIONS部分的lock_mode X locks gap before rec insert intention waiting - 用
SELECT * FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 5找出长事务 - 避免在业务关键路径上依赖
ON DUPLICATE KEY UPDATE做“存在则跳过”的逻辑,它本质是“查+锁+可能更新”,开销远高于INSERT IGNORE
自增主键 + 批量插入必须保序
InnoDB 的间隙锁(gap lock)机制决定了:只要一批 INSERT VALUES () 中的主键值乱序,就大概率触发交叉加锁。例如插入 (100, 'a'), (99, 'b'), (101, 'c'),事务 A 锁了 99–100 间隙,事务 B 锁了 100–101 间隙,互相等待。
- 应用层插入前必须
ORDER BY id ASC,不能依赖数据库排序(INSERT ... SELECT ORDER BY不影响间隙锁行为) - 单批不超过 500 行;超过则拆成多条语句,每条内部严格升序
- 禁用
UUID()或MD5()直接作主键;若必须用雪花 ID,生成后立刻在内存排序,再拼 SQL - 建表时确认
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,且没额外加UNIQUE索引(二级唯一索引会引发双倍间隙锁)
INSERT IGNORE 比 ON DUPLICATE KEY UPDATE 更轻量
INSERT IGNORE 在遇到唯一键冲突时直接跳过,不加 X 锁、不触发行更新、不维护二级索引变更日志;而 ON DUPLICATE KEY UPDATE 即使 UPDATE 子句没真改字段,也会对目标行加 X 锁,并可能引发索引分裂。
- 语义允许“冲突即丢弃”时,无条件选
INSERT IGNORE - 需要返回是否插入成功,用
ROW_COUNT()判断:返回 1 = 新增,0 = 冲突忽略 - 不要写
ON DUPLICATE KEY UPDATE id=id这种伪更新——它照样锁行,且让优化器无法走覆盖索引 - 如果业务真要更新部分字段,考虑先
SELECT ... FOR UPDATE显式加锁再UPDATE,比ON DUPLICATE KEY UPDATE更可控
分布式 ID 场景下如何避免跨节点锁竞争
雪花 ID 本身是单调递增的,但若服务实例时钟回拨、机器 ID 分配混乱、或批量插入时未按 ID 排序,就会退化为随机分布,放大间隙锁范围。更麻烦的是,按时间戳分表(如 order_202605)时,乱序插入会导致事务跨多个物理页甚至多个分区加锁。
- 所有客户端必须同步 NTP,
worker_id全局唯一且固定,禁止运行时动态分配 - 批量写入前,强制对 ID 数组调用
sort()(升序),哪怕只差 1 毫秒也要排 - 避免用时间戳做分表键,改用主键取模(如
id % 16)或一致性哈希,让同一段 ID 落在同一张表 - 如果已用时间分表,插入前先按时间字段分桶(如每 10 分钟一桶),再对每桶内数据按 ID 排序插入
真正难处理的从来不是并发量,而是主键值在 B+ 树页面中的物理落点是否连续——这点容易被监控指标掩盖,但每次页分裂都埋着锁等待的伏笔。











