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

唯一索引冲突时加 S 锁,不是设计疏漏,而是为快速失败 + 安全读取做的精准权衡:冲突发生时,你真正要操作的“老数据”没被改,只是新插入被拒,此时加 X 锁会无谓阻塞其他事务读——而业务往往需要立刻查出那条已存在的记录做提示或校验。
为什么不是X锁?S锁到底在防什么
冲突本质是“插入失败”,不是“更新已有行”。加 X 锁会阻止所有并发读(包括 SELECT ... LOCK IN SHARE MODE 和普通快照读之外的当前读),但业务常需马上读出冲突记录(比如返回“手机号已被注册”,并展示该用户昵称)。S 锁允许并发读,同时阻止其他事务删/改这条记录,守住一致性底线。
-
S锁兼容其他S锁,所以多个INSERT IGNORE可同时获得锁,不会互相阻塞 - 但
S锁与X锁互斥,因此后续UPDATE ... WHERE unique_col = ?会被阻塞,避免脏写 - 自动提交模式下,
S锁在语句结束即释放;显式事务中,它会持续到COMMIT或ROLLBACK
主键冲突 vs 唯一二级索引冲突:锁范围完全不同
这是最容易踩坑的点:主键冲突只加 S Record Lock(仅锁那一条记录),而唯一二级索引冲突加的是 S Next-key Lock(记录 + 左侧间隙),即使在 READ COMMITTED 隔离级别也不退化。
- 主键冲突:锁住冲突的
PRIMARY KEY记录本身,不涉及间隙 - 唯一二级索引(如
UNIQUE KEY name)冲突:加S Next-key Lock,覆盖该索引值所在位置及其左侧间隙,防止幻读级覆盖(例如防止另一个事务在相同name排序邻近处插入干扰唯一性判断) - 这个
S Next-key Lock是加在二级索引上,**不是主键索引**,也**不额外给主键加锁**(除非后续 UPDATE 触发)
死锁高发场景:S锁和X锁抢同一把Next-key
典型死锁链不是两个 INSERT 直接撞上,而是 INSERT IGNORE(申请 S Next-key Lock)和 UPDATE(申请 X Next-key Lock)交叉等待。两者都先拿到 S 锁(兼容),再争 X 锁,InnoDB 检测到循环等待后主动回滚其中一个。
- 事务 A:
INSERT IGNORE INTO t (name) VALUES ('alice');→ 获得uk_name上的S Next-key Lock - 事务 B:同上 → 同样获得
S Next-key Lock(S-S 兼容) - 事务 A:
UPDATE t SET level = 1 WHERE name = 'alice';→ 等待X Next-key Lock,被 B 的S锁阻塞 - 事务 B:
UPDATE t SET level = 2 WHERE name = 'alice';→ 同样等待X锁,但 A 也持S锁未释放 → 死锁触发
真正容易被忽略的是:这个 S Next-key Lock 在 READ COMMITTED 下依然存在,且锁对象是二级索引记录本身,不是主键。很多排查者盯着 data_locks 里主键锁,却漏看了唯一索引上的锁行,导致死锁分析绕弯路。


















