空表并发INSERT必死锁是因RR级别下Next-Key Lock导致所有事务在(-∞,+∞)间隙争抢Insert Intention Lock而形成循环等待;解决核心是绕过间隙锁,如用INSERT ON DUPLICATE KEY、降级到RC隔离级别或预占位。

为什么空表并发INSERT会死锁
空表时,InnoDB 的主键索引间隙是 (-∞, +∞),所有并发 INSERT 都要在这个超大间隙上申请 Insert Intention Lock。而这个锁和 Gap Lock 互斥——哪怕你只插一行,事务 A 和 B 同时尝试插入,就会互相等待对方释放间隙锁,直接形成死锁环。
这不是数据冲突,也不是语句写错,而是 RR 隔离级别下间隙锁机制的必然行为。
- 现象:两条
INSERT INTO t(name) VALUES ('a')并发执行,一个成功、一个报Deadlock found when trying to get lock; try restarting transaction - 重试后 id 跳号(比如插出 1 和 3),说明中间那个自增值被回滚丢弃了
-
SHOW ENGINE INNODB STATUS\G里能看到双方都在等PRIMARY上的X GAP锁
INSERT ON DUPLICATE KEY UPDATE 为什么也踩坑
表面看它是原子操作,但底层仍要走完整唯一约束检查流程。只要存在多个唯一索引(比如 UNIQUE(email) 和 UNIQUE(phone)),并发插入冲突值时,两个事务可能分别持有不同唯一索引的 S 锁,又互相申请对方的 X 锁,瞬间卡死。
- 错误写法:
INSERT INTO user (email, phone, name) VALUES ('a@b', '123', 'x') ON DUPLICATE KEY UPDATE name = VALUES(name) - 风险点:email 和 phone 都是
UNIQUE,InnoDB 加锁顺序不保证一致 - 更隐蔽的是:即使只用主键,只要显式指定非自增主键值(如
INSERT INTO t(id, name) VALUES (100, 'a')),也会触发 gap 锁检查
怎么让 SELECT FOR UPDATE 不变成死锁源头
SELECT ... FOR UPDATE 在 RR 下默认加 Next-Key Lock,锁住记录+前后间隙。它本身不坏,坏在加锁顺序不统一。
- 必须强制按主键升序加锁:
SELECT * FROM t WHERE status = 'pending' ORDER BY id ASC FOR UPDATE,否则不同事务扫描起点不同,锁顺序就乱了 - 避免混合使用:同一事务里别先
SELECT FOR UPDATE走status索引,再UPDATE走id主键——锁范围和顺序都不可控 - 能不用就不用:纯校验逻辑改用
SELECT ... LOCK IN SHARE MODE或应用层重试,比硬加排他锁安全得多
真正有效的规避手段有哪些
死锁不是靠“重试”解决的,而是靠控制锁粒度、锁顺序和锁范围。重点不在“怎么扛”,而在“怎么绕”。
- 降隔离级别:把事务设为
READ COMMITTED,FOR UPDATE只锁匹配行,不锁间隙——前提是业务能接受“不可重复读” - 批量插入必须排序:
INSERT INTO t (...) VALUES (...), (...), (...)的值列表,务必按主键字段升序排列,避免 gap 锁交叉 - 删掉冗余唯一索引:只保留一个业务主唯一键(如
UNIQUE(order_no)),其他字段(如phone)改用普通索引 + 应用层查重 - 长事务里禁用外部调用:HTTP 请求、文件读写、sleep 等操作会让锁持有时长不可控,直接放大死锁概率
间隙锁机制没法关,但你可以让它锁得少、锁得准、锁得顺。最常被忽略的是:空表首次并发插入、多唯一键共存、以及事务中混用不同索引路径——这三处,几乎占了线上死锁日志的七成以上。


















