唯一索引冲突时加S锁而非X锁,因冲突本质是插入失败而非修改数据,S锁允许多事务并发读但阻止删改,兼顾一致性与快速反馈;二级唯一索引加S Next-key锁(记录锁+间隙锁),防幻读级覆盖,即使RC隔离级别也不退化。

唯一索引冲突时为什么加 S 锁而不是 X 锁
因为冲突本质是插入失败,不是修改已有数据。加 X 锁会阻塞所有读操作,而业务上往往需要立刻查到冲突记录做提示(比如“手机号已注册”)或校验——S 锁允许并发读,但阻止其他事务删/改该行,刚好守住一致性底线。
常见错误现象:INSERT IGNORE 在高并发下卡住,SHOW ENGINE INNODB STATUS\G 显示大量 lock_mode S waiting——这不是锁错了,是 S 锁在排队等释放。
- 这个 S 锁加在**唯一二级索引记录上**,不是主键;
- 只在显式事务中生效(
BEGIN后的INSERT),自动提交模式下锁立刻释放,不易观察; - 即使隔离级别是
READ COMMITTED,唯一索引冲突仍会加 Next-key 锁(MySQL 官方文档明确说明,不退化)。
唯一二级索引 vs 主键索引:加锁范围为什么不同
主键冲突只加 S Record Lock(纯记录锁),而唯一二级索引冲突加的是 S Next-key Lock(记录锁 + 间隙锁)。根本区别在于:唯一二级索引不等于聚簇索引,它本身不包含完整行数据,且可能有多个值落在同一索引排序位置附近。
如果不加间隙部分,另一个事务可能在相同唯一值“周围”插入新行(例如 name='alice' 已存在,却插入 name='alice_2' 并落在相邻索引位置),干扰唯一性语义的边界判断,导致幻读级覆盖。
-
PRIMARY KEY冲突 → 加S Record Lock(仅锁那一条); -
UNIQUE KEY(非主键)冲突 → 加S Next-key Lock(锁记录 + 它左边的间隙); - 这个间隙锁是左开右闭区间,目的是防幻读,不是为了“防止重复插入”本身(重复值本身已被唯一约束拦住),而是守住索引结构的逻辑完整性。
死锁最常出现在哪里
典型死锁组合:INSERT IGNORE(申请 S Next-key 锁)和 UPDATE ... WHERE unique_col = ?(申请 X Next-key 锁)并发执行。两者都先拿到 S 锁,再争抢 X 锁,形成循环等待。
你看到的死锁日志里,事务 A 持有 S 锁、等待 X 锁,事务 B 同样持有 S 锁、等待 X 锁——它们谁都不愿先释放 S 锁,又都卡在 X 锁获取上。
- 这种死锁跟隔离级别无关,RR 或 RC 下都会发生;
- 意向锁(
IX)也会参与协调,但不会直接导致死锁,真正互斥的是行级 S/X Next-key 锁; - 用
REPLACE INTO替代INSERT IGNORE不解决问题,它内部仍是先删后插,同样触发 S 锁 + X 锁竞争。
容易被忽略的复杂点
很多人以为“唯一索引冲突只锁冲突值本身”,但实际加的是 Next-key 锁,意味着它锁住的是一段索引区间。如果你的唯一索引字段是字符串,且值分布密集(比如邀请码前缀相同),一次冲突可能锁住大片间隙,影响远超单条记录。
更隐蔽的是:这个锁加在二级索引上,不是主键上。如果后续查询走的是主键,看起来没锁,但更新唯一索引字段时仍会被阻塞——锁对象和查询路径不一致,排查时容易绕弯子。


















