本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
事务与并发(ACID / 隔离级别 / 锁 / MVCC) · 锁机制

InnoDB 的行锁、间隙锁(Gap Lock)、Next-Key Lock 各自作用?

The respective functions of InnoDB's row lock, gap lock, and next-key lock

答案语言

考察要点

这道题旨在考察你对 InnoDB 存储引擎并发控制核心机制的深度理解。面试官想看你是否真正懂得:

  1. 锁的粒度与目的:不仅仅是背诵定义,而是理解行锁(Record Lock)、间隙锁(Gap Lock)和临键锁(Next-Key Lock)各自解决了什么具体问题。
  2. 与隔离级别的关系:能清晰地将这些锁与 REPEATABLE READ(可重复读)隔离级别挂钩,并解释它们是如何协同工作以防止**幻读(Phantom Read)**的。
  3. 性能与并发的权衡:明白这种精细化锁机制在保证数据一致性的同时,对系统并发性能带来的影响以及潜在的死锁风险。

核心概念

InnoDB 在 REPEATABLE READ 隔离级别下,为解决幻读问题,引入了 Gap Lock 和 Next-Key Lock。这三种锁都作用于索引,而不是直接作用于数据行本身。

假设我们有如下数据表和索引:

sql
1CREATE TABLE `users` (
2 `id` int(11) NOT NULL AUTO_INCREMENT,
3 `name` varchar(255) DEFAULT NULL,
4 PRIMARY KEY (`id`)
5) ENGINE=InnoDB;
6
7INSERT INTO `users` (`id`, `name`) VALUES (10), (20), (30);

此时,id 主键索引中存在的值为 10, 20, 30。

1. Record Lock (行锁)

  • 定义:也叫记录锁,是针对单个索引记录的锁。它锁住的是索引,而非数据行本身。
  • 作用:防止其他事务对同一索引记录进行 UPDATE 或 DELETE 操作。
  • 触发条件:当 SQL 查询使用了唯一性索引(PRIMARY KEY 或 UNIQUE KEY)进行等值查询且精准命中一条记录时,会将 Next-Key Lock 降级为 Record Lock。
  • 示例:
    sql
    1-- 事务 A
    2BEGIN;
    3SELECT * FROM users WHERE id = 10 FOR UPDATE;
    4-- 此时,事务 A 会在 id=10 的主键索引记录上加一个 Record Lock。
    5-- 事务 B 执行 UPDATE users SET name = 'new_name' WHERE id = 10; 将会阻塞。

2. Gap Lock (间隙锁)

  • 定义:锁定一个开区间,即两个索引记录之间的间隙,或者第一个索引记录之前的间隙,或最后一个索引记录之后的间隙。
  • 作用:防止其他事务在这个间隙中 INSERT 新的记录,从而避免幻读。Gap Lock 本身不限制其他事务对间隙中已有记录的读写。
  • 核心特性:Gap Lock 之间是不互斥的。即事务 A 持有间隙 (10, 20) 的 Gap Lock,事务 B 也可以同时持有该间隙的 Gap Lock。它们共同的目的是阻止第三方 INSERT。
  • 触发条件:范围查询、或者非唯一索引上的等值查询,都可能产生 Gap Lock。
  • 示例:
    sql
    1-- 事务 A
    2BEGIN;
    3SELECT * FROM users WHERE id > 10 AND id < 20 FOR UPDATE;
    4-- 此时,事务 A 会在 (10, 20) 这个开区间上加一个 Gap Lock。
    5-- 事务 B 执行 INSERT INTO users (id, name) VALUES (15, 'new_user'); 将会阻塞。

3. Next-Key Lock (临键锁)

  • 定义:Record Lock + Gap Lock 的组合,锁定一个左开右闭的区间。它是 InnoDB 在 REPEATABLE READ 隔离级别下默认的加锁单位。

  • 作用:既能锁定索引记录本身(Record Lock 的功能),又能锁定该记录之前的间隙(Gap Lock 的功能),从而同时防止记录被修改和间隙被插入。

  • 触发条件:在 REPEATABLE READ 下,使用普通索引或范围查询时,默认使用的就是 Next-Key Lock。

  • 示例:

    sql
    1-- 事务 A
    2BEGIN;
    3SELECT * FROM users WHERE id >= 20 AND id < 30 FOR UPDATE;

    根据 users 表的现有数据 (10, 20, 30),InnoDB 的索引结构可以看作如下区间: (-∞, 10], (10, 20], (20, 30], (30, +∞)

    上述查询 id >= 20 AND id < 30 会扫描到 id=20 的记录,因此会锁定覆盖 id=20 的 Next-Key Lock,即 (10, 20]。同时,扫描会继续向后直到 id=30,因此还会锁定覆盖 id=30 的 Next-Key Lock,即 (20, 30]。

    • 锁 (10, 20]:保护了记录 20,并阻止在 (10, 20) 间插入。
    • 锁 (20, 30]:保护了记录 30,并阻止在 (20, 30) 间插入。

    最终结果是,其他事务无法 UPDATE/DELETE id=20 和 id=30 的记录,也无法在 (10, 30) 这个大区间内 INSERT 任何新记录。

锁的降级与版本差异

  • 锁降级:当使用唯一索引进行等值查询且记录存在时,Next-Key Lock 会降级为 Record Lock,因为不需要保护间隙。如果记录不存在,则降级为 Gap Lock。
  • 版本差异:
    • 在 READ COMMITTED 隔离级别下,Gap Lock 和 Next-Key Lock 会被禁用(除了少数特殊情况,如外键约束检查),从而提高并发度,但无法避免幻读。
    • MySQL 8.0 引入了 SKIP LOCKED 和 NOWAIT 选项,允许在 SELECT ... FOR UPDATE/SHARE 时跳过已被锁定的行,提供了更灵活的并发控制。

常见低分回答

  1. 混淆概念:将 Next-Key Lock 描述为“锁住一行和它旁边的行”,这种描述非常不精确,正确的说法是“锁住索引记录本身以及其之前的间隙”。
  2. 忽略与隔离级别的关系:只谈锁的定义,不提它们是在 REPEATABLE READ 级别下为解决幻读而存在的。这是一个巨大的失分点,表明理解不够深入。
  3. 误解 Gap Lock 的冲突性:认为“一个事务持有 Gap Lock,另一个事务就不能再获取同一个 Gap Lock”。这是错误的,Gap Lock 之间是兼容的,它们共同阻止 INSERT。
  4. 认为锁是加在数据行上的:反复强调“锁住了数据行”,而忽略了 InnoDB 的锁是实现在索引上的。如果 WHERE 条件中的列没有索引,会导致全表扫描,进而可能锁住整张表的所有间隙,造成灾难性的并发问题。
  5. 对 Next-Key Lock 的范围描述不清:无法清晰地说出 (A, B] 这种左开右闭的区间特性,只是模糊地说“锁住了一个范围”。

工程实践与优化

  • 死锁的根源:Next-Key Lock 是导致死锁的常见原因。例如,两个事务以相反的顺序请求相邻区间的锁,很容易形成循环等待。
    text
    1-- 经典死锁场景
    2-- T1: UPDATE users SET name='a' WHERE id = 25; (请求 (20, 30] 的锁)
    3-- T2: UPDATE users SET name='b' WHERE id = 15; (请求 (10, 20] 的锁)
    4-- T1: INSERT INTO users (id) VALUES (18); (等待 T2 释放 (10, 20] 的锁)
    5-- T2: INSERT INTO users (id) VALUES (28); (等待 T1 释放 (20, 30] 的锁) -> Deadlock!
  • 性能影响与优化:
    1. 选择合适的隔离级别:如果业务场景可以容忍幻读,将隔离级别设置为 READ COMMITTED 是最直接的优化,可以禁用大部分 Gap Lock,显著提升并发性能。
    2. 优化查询语句:尽可能使用唯一索引进行等值查询,这样只会加上 Record Lock,锁的粒度最小。避免不必要的范围查询。
    3. 缩短事务:大事务会长时间持有锁,严重影响并发。应将事务拆小,并尽快提交。
    4. 查看锁信息:
    • MySQL 5.7 及之前:SHOW ENGINE INNODB STATUS; 在 LATEST DETECTED DEADLOCK 和 TRANSACTIONS 部分可以查看锁等待和死锁信息。
    • MySQL 8.0+:使用 performance_schema 下的 data_locks 和 data_lock_waits 表,可以更结构化、更清晰地查询当前的锁和锁等待情况。

常见追问与延伸题目

  1. 追问:在我的业务中,如何主动避免 Gap Lock 来提升并发?

    • 回答要点 1 (治本):评估业务是否能接受幻读。如果可以,将数据库的默认隔离级别或当前会话的隔离级别设置为 READ COMMITTED。SET tx_isolation = 'READ-COMMITTED'; 这是最有效的方法。
    • 回答要点 2 (治标):如果必须使用 REPEATABLE READ,那就从 SQL 入手。确保所有高频的并发更新、删除操作都通过主键或唯一索引的等值匹配来执行。例如,将 UPDATE ... WHERE order_status = 'processing' LIMIT 1; 改造为 SELECT id FROM ... WHERE order_status = 'processing' LIMIT 1 FOR UPDATE; 然后再 UPDATE ... WHERE id = ?;。
  2. 追问:UPDATE users SET name = 'test' WHERE id = 15; (id=15 的记录不存在) 在 RR 级别下会加什么锁?

    • 回答要点:这是一个非常好的问题,考察了对锁降级的理解。因为 id 是主键(唯一索引),InnoDB 会首先定位到 id=15 应该在哪个间隙。根据我们的数据 (10, 20, 30),id=15 位于 (10, 20) 之间。由于记录不存在,Next-Key Lock (10, 20] 会降级为**Gap Lock**,锁住 (10, 20) 这个开区间。这么做的目的是防止其他事务并发 INSERT 一条 id=15 的记录,从而导致当前事务再次执行时出现幻读。
  3. 追问:什么是插入意向锁(Insert Intention Lock)?它和 Gap Lock 是什么关系?

    • 回答要点 1 (定义):插入意向锁是一种特殊的 Gap Lock,它是在 INSERT 操作执行前,由 INSERT 语句自己设置的。如果 INSERT 的位置已经被其他事务加了 Gap Lock 或 Next-Key Lock,那么 INSERT 操作就需要等待,此时它持有的就是插入意向锁。
    • 回答要点 2 (关系):插入意向锁之间不互斥。多个事务可以同时在同一个间隙中等待,持有各自的插入意向锁(只要它们插入的不是同一个键值)。但是,插入意向锁与 Gap Lock / Next-Key Lock 是互斥的。这精妙地实现了:当一个间隙没有被锁时,允许多个事务并发插入;一旦间隙被锁,所有想插入的事务都必须等待。