[面试题] SELECT FOR UPDATE忘加索引会发生什么?间隙锁怎么导致死锁?
版块 A:知识讲解 (Knowledge Recap)
InnoDB 行级锁的三把锁:
| 锁类型 | 锁住的范围 | 触发场景 |
|---|---|---|
| 记录锁 (Record Lock) | 锁住具体某一行记录 | 唯一索引等值查询,且数据存在 |
| 间隙锁 (Gap Lock) | 锁住两条记录之间的间隙(防止插入) | 普通索引等值查询 / 等值查询数据不存在 |
| 临键锁 (Next-Key Lock) | 记录锁 + 间隙锁的组合 | InnoDB 默认加锁单位(RR隔离级别下) |
两个关键的锁退化规则:
唯一索引 + 等值查询 + 数据存在→ Next-Key Lock 退化为 Record Lock(不锁间隙)任意索引 + 等值查询 + 数据不存在→ Next-Key Lock 退化为 Gap Lock(只锁间隙)
致命陷阱:Gap Lock 不互斥!两个事务可以同时持有同一范围的 Gap Lock,但都想插入数据时 → 互相等待 → 死锁 无索引的 SELECT FOR UPDATE:where 条件没有索引 → 全表扫描 → 每行都加 Next-Key Lock → 等价于表锁
版块 B:面试实战 (Combat Script)
Q:InnoDB 的行锁锁的到底是什么?SELECT FOR UPDATE 忘了加索引会怎样?
破局思路:面试官在考察你是否真正理解”行锁锁索引”这个本质。能从这里延伸到间隙锁死锁的真实案例,才是高手的标志。
💬 高手逐字稿
关于这个问题,核心结论是:InnoDB 的行锁,锁的是索引,而不是数据本身。
这带来了一个非常重要的推论:如果你的 SELECT FOR UPDATE 的 where 条件里没有走索引,InnoDB 没办法精确锁住某一行,就只能退化成全表扫描,把每一行、每一个间隙全部加锁,效果上等价于表级锁,并发性能直接崩掉。
说到加锁,我想延伸讲一下间隙锁导致死锁的问题,因为这是我们在生产环境中真实踩过的坑。
InnoDB 在 RR 隔离级别下,加锁的基本单位是临键锁(Next-Key Lock)。但有一个特殊情况:当等值查询没有命中任何数据时,临键锁会退化成间隙锁(Gap Lock),只锁住那段间隙,不锁记录。
这里有一个很多人不知道的关键细节,那就是:间隙锁之间是不互斥的。两个事务可以同时持有同一段间隙的 Gap Lock。
这就导致了一个经典的死锁场景:事务 A 和事务 B 都通过 SELECT FOR UPDATE 查询了一条不存在的数据,于是都拿到了同一段间隙的 Gap Lock。接着,事务 A 要插入数据,被事务 B 的 Gap Lock 阻塞;同时事务 B 也要插入,被事务 A 的 Gap Lock 阻塞。两边互相等待,死锁就发生了。
解决方案有两个:第一,把隔离级别降到 RC,RC 下没有幻读问题,自然就不会有 Gap Lock;第二,用分布式锁在业务层保证串行化,彻底绕开数据库锁的竞争。