唯一索引等值查询记录存在时退化为记录锁,根本原因是唯一性可杜绝幻读,无需锁间隙;记录不存在时退化为间隙锁;而普通索引无法保证唯一性,必须用Next-Key Lock封锁整个区间以防幻读。

唯一索引等值查询时锁退化为记录锁,普通索引则锁区间
根本原因在于:唯一索引能**精确命中且仅命中一条记录**,InnoDB 无需防范“幻读插入”,所以直接加 record lock;而普通索引无法保证唯一性,必须用 next-key lock(记录锁 + 间隙锁)封锁可能插入重复值的间隙。
典型现象:SELECT * FROM t WHERE uk_col = 123 FOR UPDATE(uk_col 是唯一索引)只锁住那一条匹配记录;但同样语句作用于普通索引字段时,会锁住该值所在区间,比如 [100, 200) —— 即使表中只有 123 这一行,105、199 的插入也会被阻塞。
- 唯一索引查到存在记录 → 退化为
record lock - 唯一索引查不到记录 → 退化为
gap lock(只锁间隙,不锁记录) - 普通索引无论是否存在 → 默认走
next-key lock,范围是左闭右开(如[a, b))
INSERT ON DUPLICATE KEY UPDATE 触发唯一索引的间隙锁
这个语句表面是“更新”,实际执行路径包含“先查再判再插/更”,InnoDB 必须防止其他事务在检查间隙里插入相同值。所以即使目标行不存在,也会对对应间隙加 next-key lock,而非简单 gap lock。
容易踩的坑:INSERT ... ON DUPLICATE KEY UPDATE 在高并发下容易引发死锁,尤其当多个事务同时操作同一段间隙(比如都试图插入 email = 'a@b.com'),各自持有了部分间隙锁又等待对方释放。
- 普通索引上执行相同语句 → 不触发唯一性校验 → 不加间隙锁,只走普通写入路径
- 唯一索引 +
NULL值 → 因为NULL != NULL,多个INSERT ... (email = NULL)不会相互阻塞,间隙锁不生效 - 若业务已用分布式锁做幂等,再叠加此语句 + 唯一索引 → 锁竞争和死锁概率双升,纯属冗余
联合唯一索引的锁行为更复杂
联合唯一索引(如 UNIQUE KEY uk_a_b (a, b))的加锁不是简单按单列拆分,而是以整个键值为单位判断“是否存在”。只要任意一列不同,就视为不冲突;但全 NULL 的行仍可重复插入,且不会触发间隙锁。
示例:现有记录 (1, NULL) 和 (NULL, 2),再插入 (1, 2) 会正常通过;但插入 (1, NULL) 第二次就会报错,并在 (1, NULL) 对应位置加 record lock;而插入 (NULL, NULL) 可无限次成功,且不加任何锁。
- 联合索引中某列有值、另一列为
NULL→ 按实际值定位,锁行为与单列唯一索引一致 - 两列全
NULL→ InnoDB 不做唯一性校验,也不加锁,只走普通插入流程 - 联合普通索引 → 依然按
next-key lock锁区间,但区间边界由最左前缀决定
change_buffer 不生效是锁差异的底层动因
唯一索引强制“先读后判”才能校验唯一性,这个读操作绕过 change_buffer,必须把页从磁盘拉进 buffer_pool,自然也就绕过了缓存层带来的锁延迟缓冲 —— 所以锁的获取更早、更重、更确定;而普通索引更新可暂存 change_buffer,锁只在真正 merge 到页时才施加,延迟且分散。
这意味着:在机械硬盘或 buffer_pool 不足的实例上,唯一索引的锁争抢会更集中、更可见;SSD 上虽 I/O 延迟低,但高并发下锁等待时间仍比普通索引长 2–3 倍(实测常见于 innodb_row_lock_time_avg 监控项)。
-
innodb_change_buffering = inserts时,唯一索引的 INSERT 依然无法缓存,普通索引可缓存 - 锁等待堆栈里频繁出现
dict_table_wait_for_background_drop_table或row_ins_duplicate_error→ 往往指向唯一索引校验路径卡住 - 别只看
show engine innodb status里的 lock wait,要结合performance_schema.data_locks查具体锁类型和范围
唯一索引的锁逻辑始终围绕“数据规则”展开,普通索引只服务“访问路径”——这个根本定位差异,决定了它们在加锁时机、范围、粒度上的所有区别。实际调优时,不能只盯着 SQL 写法,得先确认那个索引到底是约束还是加速器。


















