唯一二级索引冲突时,InnoDB 必须加 S Next-key Lock 以锁定 (prev_value, current_value] 区间,防止邻近值插入导致唯一性检查漏判;主键冲突只需 S Record Lock,因聚簇索引物理顺序与值顺序一致。

批量 UPSERT(如 INSERT ... ON DUPLICATE KEY UPDATE)在唯一二级索引冲突时,InnoDB 必须加 S Next-key Lock,这不是“升级”,而是它从一开始就必须这么锁——否则无法保证唯一性不被并发插入破坏。
为什么唯一二级索引冲突必须加 S Next-key Lock
只锁住冲突记录本身(Record Lock)是危险的:B+ 树中值有序但物理分散,另一个事务仍可在紧邻位置插入排序上“挨着”的值(比如已存在 name = 'alice',却允许插入 'alicia' 或 'ali'),导致后续唯一性检查漏判。InnoDB 要确保“这个值在整个索引顺序中是孤立的”,就必须锁定左开右闭区间 (prev_value, current_value]——这正是 Next-key Lock 的语义。
主键冲突只需 S Record Lock,因为聚簇索引的值和物理顺序一致,不存在“邻近干扰”问题;而 UNIQUE KEY 是非聚簇二级索引,必须靠间隙锁定堵住漏洞。
-
READ COMMITTED隔离级别下该行为也不退化——MySQL 官方文档明确要求唯一二级索引冲突必须加Next-key Lock -
INSERT IGNORE和ON DUPLICATE KEY UPDATE都绕不开这个锁:它们只是让 SQL 不报错,但唯一性检查阶段仍要先申请S Next-key Lock确认“没人占着”,才能决定走插入还是更新路径
批量 UPSERT 为什么会放大锁竞争
单条 UPSERT 可能只锁一个区间,但批量执行(如 INSERT ... ON DUPLICATE KEY UPDATE 带多行 VALUES)会按索引顺序逐行检查唯一性,每行都可能触发独立的 S Next-key Lock 申请。若这批值在索引中物理相邻(例如 order_sn 前缀相同、后缀递增),它们各自申请的锁区间极易重叠。
- 两个事务并发执行
INSERT ... ON DUPLICATE KEY UPDATE,分别插入('SN_10086')和('SN_10087'),若这两个值在 B+ 树叶子页中紧邻,它们的Next-key Lock区间可能都覆盖(SN_10085, SN_10087],形成 S 锁等待链 - 更危险的是混合操作:事务 A 拿到
uk_order_sn上'SN_10086'的S Next-key Lock后,立刻执行UPDATE ... WHERE order_sn = 'SN_10086'(需X Next-key Lock);事务 B 同样拿到 S 锁后也尝试升级为 X 锁——此时双方都在等对方释放 S 锁以获取 X 锁,InnoDB 判定死锁
如何验证和规避批量 UPSERT 的 Next-key 锁影响
别依赖 SHOW ENGINE INNODB STATUS 里模糊的 lock_mode X locks rec but not gap 描述,它常漏掉间隙部分。真正可靠的方式是结合执行计划与并发压测:
- 用
EXPLAIN看INSERT ... ON DUPLICATE KEY UPDATE是否命中唯一索引:type应为const或eq_ref;若为range或ALL,说明实际在全表扫描并逐行加锁 - 手动验证锁范围:会话 A 执行批量 UPSERT,会话 B 立即尝试插入一个在目标唯一值“左侧间隙”中的值(如目标是
'SN_10086',则试插'SN_100855'),若被阻塞,说明间隙已被锁住 - 高频防重场景(如工单流水号、邀请码绑定)慎用批量 UPSERT;改用主键或自增 ID 先插入,再用
SELECT ... FOR UPDATE+ 应用层判断,把锁粒度控制在单行 Record Lock 级别
最易被忽略的一点:即使你写的 SQL 看似只操作一行,只要涉及唯一二级索引且发生冲突,InnoDB 就会无条件加 S Next-key Lock——它不看你有没有写 FOR UPDATE,也不看你隔离级别设成什么,这是引擎层保障语义安全的硬性规则。


















