唯一索引冲突时加s锁而非x锁,因冲突本质是插入失败而非修改数据,s锁允许多个事务并发读但阻止删改,兼顾一致性与响应速度;二级唯一索引加s next-key锁(非纯记录锁),为防幻读级覆盖,即使rc隔离级别也不退化。

唯一索引冲突时为什么加 S 锁而不是 X 锁
因为冲突本身不是“要改老数据”,而是“新插入失败”,加 X 锁会无谓阻塞读——业务上往往需要立刻感知冲突(比如提示用户“该手机号已注册”),同时还要能查到原记录做校验或展示。S 锁允许并发读,但阻止其他事务删/改那条冲突记录,刚好守住一致性底线。
常见错误现象:INSERT IGNORE 或 REPLACE INTO 在高并发下突然卡住,SHOW ENGINE INNODB STATUS\G 显示大量 lock_mode S waiting ——这不是锁错了,是 S 锁正在排队等释放。
注意:这个 S 锁是加在**唯一二级索引记录上**,不是主键;且只在事务中发生(即显式 BEGIN 后的 INSERT),自动提交模式下锁会立刻释放,不易观察。
为什么二级唯一索引加的是 S Next-key 锁,不是纯记录锁
Next-key 锁 = 记录锁 + 间隙锁,左开右闭。对唯一二级索引加 Next-key 锁,核心是为了防止“幻读级覆盖”:比如 name='alice' 已存在,若只加记录锁,另一个事务仍可能在相同 name 值附近插入新行(虽然值不同,但索引排序位置紧邻),干扰唯一性语义的边界判断。
使用场景典型如秒杀名额、邀请码绑定——你不能只防住“完全相同的值”,还要防住“在它前后插入导致逻辑错乱”的情况。
关键差异点:
-
PRIMARY KEY冲突 → 加 S Record Lock(仅锁那一条) -
UNIQUE KEY(非主键)冲突 → 加 S Next-key Lock(锁记录 + 它左边的间隙) - 即使隔离级别是
READ COMMITTED,唯一索引冲突仍会加 Next-key 锁(MySQL 文档明确说明,不因 RC 而退化)
死锁常出现在 S 锁与 X 锁交叉等待
最典型的死锁组合:INSERT IGNORE(申请 S Next-key 锁)和 UPDATE ... WHERE unique_col = ?(申请 X Next-key 锁)并发执行。两者都先拿到 S 锁,再争抢 X 锁,形成循环等待。
示例复现步骤:
- 事务 A:
INSERT IGNORE INTO t1 (name, level) VALUES ('A', 0);→ 成功获得uk_name上的 S Next-key 锁 - 事务 B:
INSERT IGNORE INTO t1 (name, level) VALUES ('A', 0);→ 同样获得 S Next-key 锁(S-S 兼容) - 事务 A:
UPDATE t1 SET level = 1 WHERE name = 'A';→ 等待 X 锁,被 B 的 S 锁阻塞 - 事务 B:
UPDATE t1 SET level = 2 WHERE name = 'A';→ 同样等待 X 锁,但此时 A 持有 S 锁未释放,B 也持 S 锁,InnoDB 判定死锁
这种模式在重试逻辑里特别危险:消息队列重复投递 + INSERT IGNORE + 后续状态更新,三者一凑齐就容易触发。
排查和缓解的关键点
真正容易被忽略的不是“怎么加锁”,而是“锁什么时候释放”。S Next-key 锁在事务结束(COMMIT 或 ROLLBACK)前一直持有,哪怕 INSERT 早已报错返回。
实操建议:
- 用
SELECT * FROM performance_schema.data_locks WHERE OBJECT_SCHEMA = 'db' AND OBJECT_NAME = 't1'\G查实时锁,注意看LOCK_MODE和LOCK_DATA - 避免在长事务里混用
INSERT IGNORE和基于同一唯一索引的UPDATE - 若业务允许,把唯一约束检查前置到应用层(如先
SELECT再决定是否INSERT),但要注意SELECT ... FOR UPDATE会加 X 锁,代价更高 - 写重试逻辑时,不要无脑重试
INSERT IGNORE,应捕获ERROR 1062 (23000)后直接跳过或走更新分支
复杂点在于:Next-key 锁的“间隙”部分不可见、不可控,你看到的 LOCK_DATA 只显示记录值,但实际封锁范围得靠索引顺序推算——这正是线上死锁难以复现又难以根治的原因。











