InnoDB 的行锁、间隙锁和 next-key lock 有什么区别?
简化版
行锁锁的是已有索引记录,间隙锁锁的是索引记录之间的空隙,next-key lock 是行锁加间隙锁的组合,用来防止其他事务在范围内插入新记录。InnoDB 的锁通常是加在索引上的,能否命中合适索引会直接影响锁范围。
详细版
三类锁可以这样理解:
- 记录锁(record lock):锁住某条索引记录,比如主键
id = 10。 - 间隙锁(gap lock):锁住两个索引记录之间的间隙,比如
(10, 20),防止插入新记录。 - next-key lock:记录锁 + 前面的间隙锁,常见形式是
(10, 20],既锁记录又锁间隙。
InnoDB 在 RR 隔离级别下,为了防止当前读的幻读,范围查询可能使用 next-key lock。比如:
select * from orders where id between 10 and 20 for update;
它不只锁已有的记录,还可能锁住相关范围的间隙,防止其他事务插入新的 id=15 记录。
完整版教学
一、InnoDB 锁为什么和索引强相关
InnoDB 的行级锁是锁索引记录,而不是直接锁“这一行的物理位置”。如果查询走主键索引,就锁主键索引记录;如果走二级索引,可能锁二级索引记录,并在需要时关联到聚簇索引记录。
这解释了一个常见现象:明明只是更新几行,SQL 写得不好却锁住很多行。因为没有合适索引时,InnoDB 可能扫描大量记录,锁范围随之扩大。
例如 update user set status=1 where phone='13800000000',如果 phone 有唯一索引,InnoDB 可以精确锁住那条索引记录;如果没有索引,优化器可能扫描更多记录,锁冲突范围就会被放大。面试回答要把“锁是行级”补充成“锁加在索引记录上”,否则很容易解释不了为什么缺索引会导致锁等待变严重。
| SQL 条件 | 索引情况 | 锁范围倾向 |
|---|---|---|
where id = 10 | 主键命中且存在 | 精确记录锁 |
where uk = 'A' | 唯一索引命中且存在 | 通常记录锁 |
where age between 20 and 30 | 普通索引范围 | 范围上的 next-key lock |
where name = 'Tom' | 无合适索引 | 可能扫描并锁更多记录 |
二、记录锁:锁住已经存在的记录
记录锁最容易理解。比如:
select * from user where id = 10 for update;
如果 id 是主键且记录存在,InnoDB 锁住 id=10 这条索引记录。其他事务不能修改或删除这条记录,直到当前事务提交或回滚。
记录锁保护的是已有索引项。假设事务 A 执行 select * from user where id=10 for update 并且记录存在,事务 B 再执行 update user set age=20 where id=10 会等待;但事务 B 插入 id=11 通常不受这条记录锁影响。这个边界很重要:记录锁管已有记录,不能单独防止范围里冒出新行。
三、间隙锁:锁住“不存在的位置”
间隙锁锁的是范围空隙,目的是防止其他事务往这个空隙里插入新记录。
假设表里已有 id:
10, 20, 30
区间 (10, 20) 就是一个间隙。如果事务 A 锁住这个间隙,事务 B 想插入 id=15 会被阻塞。注意,间隙锁不是为了保护某条已存在记录,而是为了保护范围不被插入新记录。
间隙锁的存在,是为了配合当前读解决幻读。比如事务 A 先查 id between 10 and 20 for update,如果只锁已有记录,事务 B 完全可以插入 id=15;事务 A 再查同一范围,就会多出一行。间隙锁通过锁住“还没有记录的位置”,阻止这种新插入。
已有索引值:10 20 30
间隙范围: (10,20) (20,30)
锁住 (10,20) 后,插入 15 会等待;
但修改已存在的 20 是否等待,要看是否也锁了记录 20。
四、next-key lock:记录和间隙一起锁
next-key lock 可以理解为“左开右闭”的锁范围,比如 (10, 20]。它既锁住 20 这条记录,也锁住 10 到 20 之间的空隙。
在 RR 下,范围当前读为了避免幻读,常常需要 next-key lock。如果只锁已有记录,别人仍然能往范围里插入新记录,下一次当前读就可能多出一行。
假设索引上已有 10、20、30,执行 where id > 10 and id <= 20 for update,一个典型理解是锁住 (10,20]:既不允许别人修改 20 这条记录,也不允许别人往 10 和 20 中间插入新值。真实锁边界会受查询条件、索引类型和优化器选择影响,但 next-key lock 的核心就是“记录 + 前方间隙”。
记忆钩子:记录锁防别人改已有行,间隙锁防别人插新行,next-key lock 是把“已有行”和“前面的空位”一起保护起来。
五、唯一索引等值查询会不会有间隙锁
如果使用唯一索引做等值查询,并且记录存在,InnoDB 通常可以退化为记录锁,因为它已经精确定位到唯一记录,不需要锁范围防插入。
但如果唯一索引等值查询的记录不存在,为了防止其他事务插入这个值,仍可能锁住对应间隙。实际锁行为还会受隔离级别、语句类型、索引选择影响,不能只凭 SQL 表面判断。
这个点是面试追问高频。唯一索引等值命中且记录存在时,InnoDB 知道整个表里最多就这一条,不需要锁住周围范围;但查一个不存在的唯一值,例如已有 id=10,20,事务 A 执行 select * from t where id=15 for update,为了保证当前读语义,事务 B 插入 id=15 可能会被对应间隙锁挡住。
| 场景 | 常见锁行为理解 | 原因 |
|---|---|---|
| 唯一索引等值,记录存在 | 可退化为记录锁 | 已精确命中唯一记录 |
| 唯一索引等值,记录不存在 | 可能锁间隙 | 防止其他事务插入这个值 |
| 普通索引等值 | 可能锁多个记录及间隙 | 普通索引值可重复 |
| 范围当前读 | 常见 next-key lock | 防止范围内新增幻影记录 |
六、锁相关的优化思路
减少锁冲突,通常不是盲目调隔离级别,而是:
- 给
where条件建立合适索引,缩小扫描和锁范围; - 事务尽量短,拿锁后尽快提交;
- 热点更新按固定顺序访问,降低死锁概率;
- 对范围更新特别谨慎,先评估会锁哪些区间。
优化锁冲突要先从 SQL 和索引入手。比如 update orders set status='CANCELLED' where user_id=? and status='INIT',如果缺少 (user_id,status),可能扫描很多订单;加上合适索引后,扫描范围和锁范围都会下降。事务内部也不要混入远程调用、文件处理、人工确认这类慢动作,否则锁持有时间会被无意义拉长。
七、常见误区与追问
- 误区:InnoDB 行锁就是锁物理行,和索引无关。 InnoDB 行锁加在索引记录上,走什么索引、扫多少索引记录,会直接影响锁范围。
- 误区:间隙锁会锁住某条具体记录。 间隙锁锁的是两个索引值之间的空隙,主要阻止插入,不是保护已有记录内容。
- 误区:RR 下所有查询都会加 next-key lock。 普通快照读依靠 MVCC,不加锁;
for update、update、delete这类当前读才会涉及锁。 - 误区:唯一索引等值查询永远没有间隙锁。 命中已存在唯一记录时通常可退化为记录锁,但查询不存在值时仍可能锁对应间隙。
- 追问:next-key lock 为什么常说是左开右闭? 因为它把某条索引记录和它前面的间隙组合起来,典型表示为
(前一个索引值, 当前索引值]。 - 追问:如何减少 next-key lock 带来的阻塞? 使用更精确的唯一索引或更高选择性的联合索引缩小扫描范围,避免大范围当前读,并控制事务时长。
八、加强记忆
记录锁锁已有索引记录,间隙锁锁两个索引值之间的空位,next-key lock 把“空位 + 右侧记录”一起锁住,用来支撑 RR 下当前读的范围稳定性。InnoDB 锁跟索引走,所以锁问题不能只看 SQL 文本,还要看执行计划、索引命中和扫描范围。真正会用这题的人,能把幻读、范围查询、缺索引锁扩大和死锁风险放在同一条线上解释。