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

RR(可重复读)是否解决了幻读?InnoDB 是怎么用 next-key lock 处理幻读的?

Does RR (Repeatable Read) solve phantom reads? How does InnoDB use next-key locks to

答案语言

考察要点

这道题面试官想看你是否深刻理解事务的隔离级别,特别是**可重复读(Repeatable Read)**的定义和实现。面试官期待你不仅知道 SQL 标准的定义,更能清晰地阐述 InnoDB 存储引擎是如何通过 MVCC(多版本并发控制) 和 Next-Key Lock(临键锁) 这两大核心机制,在实践中“超额”完成标准,从而在很大程度上解决了幻读问题。

核心概念

要回答这个问题,首先需要精准定义以下几个核心概念:

1. 幻读 (Phantom Read)

幻读是指在一个事务(T1)内,多次执行同一个范围查询,但第二次查询返回了第一次查询时不存在的新行。这些新行是由另一个并发事务(T2)在此期间插入并提交的。

关键点:幻读关注的是行数的增减,而不可重复读关注的是同一行数据的变更。

数据示例: 假设有一个 users 表:

sql
1CREATE TABLE users (
2 id INT PRIMARY KEY AUTO_INCREMENT,
3 name VARCHAR(50),
4 age INT,
5 INDEX idx_age (age)
6);
7INSERT INTO users (name, age) VALUES ('Alice', 20), ('Bob', 30);
事务 T1 (RR 级别)事务 T2
START TRANSACTION;
SELECT * FROM users WHERE age > 25; <br>-- 返回 1 行 (Bob, 30)
START TRANSACTION;
INSERT INTO users (name, age) VALUES ('Charlie', 28);
COMMIT;
SELECT * FROM users WHERE age > 25; <br>-- 再次查询

在标准的 RR 定义下,T1 第二次查询可能会看到 Charlie 这条新纪录,结果集从 1 行变为 2 行,如同出现了幻影,这就是幻读。

2. SQL 标准 vs. InnoDB 实现

  • ANSI SQL 标准:标准定义的 RR 级别允许幻读的发生。它只要求在一个事务中,对同一行数据的多次读取结果必须一致(防止不可重复读)。
  • MySQL InnoDB 实现:InnoDB 存储引擎在 RR 级别下,通过特定的机制大部分情况都避免了幻读,使其表现得更接近于 SERIALIZABLE 级别。

3. MVCC (多版本并发控制)

MVCC 是 InnoDB 实现“快照读”的基础,也是其在 RR 级别下防止幻读的第一道防线。

  • 工作原理:当一个事务启动时(在 RR 级别下),InnoDB 会创建一个一致性视图(Read View)。该事务之后的所有快照读(普通的 SELECT 语句)都将基于这个 Read View,只能看到在创建 Read View 之前已经提交的数据。之后其他事务所做的任何 INSERT, UPDATE, DELETE 操作,即使已经提交,对于当前事务的快照读来说都是不可见的。
  • 如何防止幻读:对于上面例子中的普通 SELECT 查询,由于 T1 的 Read View 在 T2 插入数据前已经形成,T2 提交的新数据 ('Charlie', 28) 对 T1 是不可见的。因此,T1 无论执行多少次 SELECT * FROM users WHERE age > 25;,都只会看到 (Bob, 30) 这一行。

结论:在**快照读(Snapshot Read)**场景下,MVCC 已经完美解决了幻读问题。

4. 当前读 (Current Read) 与 Next-Key Lock

然而,事务中不只有快照读,还有当前读。当前读读取的是数据库的最新版本,并且会对读取的记录加锁。常见的当前读语句包括:

  • SELECT ... FOR UPDATE
  • SELECT ... LOCK IN SHARE MODE
  • INSERT, UPDATE, DELETE

如果只靠 MVCC,当前读场景下依然会发生幻读。例如,T1 执行 SELECT ... FOR UPDATE,T2 插入数据并提交,T1 再次执行 SELECT ... FOR UPDATE 就会看到新行。

为了解决这个问题,InnoDB 引入了 Next-Key Lock(临键锁)。

  • 定义:Next-Key Lock 是 Record Lock(记录锁) 和 Gap Lock(间隙锁) 的结合体。它既锁住记录本身,也锁住记录之前的那个间隙。
  • 公式:Next-Key Lock = Record Lock + Gap Lock
  • 工作原理:当 InnoDB 执行一个当前读时,它会根据 WHERE 条件在索引上定位。它不仅会给匹配到的索引记录加上 Record Lock,还会给这些记录之间的间隙加上 Gap Lock,以及最后一个记录到正无穷之间的间隙加上 Gap Lock。
  • 如何防止幻读:
    • 假设索引 idx_age 上有值为 20 和 30 的记录。
    • 事务 T1 执行 SELECT * FROM users WHERE age = 30 FOR UPDATE;
    • InnoDB 会在 age=30 的记录上加一个 Next-Key Lock。这个锁实际上锁定的范围是 (20, 30]。
    • 如果 T1 执行 SELECT * FROM users WHERE age > 25 FOR UPDATE;,它会在 age=30 的记录上加锁,并对 (30, +∞) 这个区间加 Gap Lock。
    • 此时,任何其他事务(如 T2)想在这个被锁定的间隙 (20, 30] 或 (30, +∞) 内 INSERT 一条新记录(例如 age=28 或 age=40),INSERT 操作会被阻塞,直到 T1 提交或回滚。
    • 这样,就阻止了新行的插入,从而防止了 T1 在后续的当前读中看到“幻影”行。

ASCII 图示 Next-Key Lock: 索引 idx_age 上的值:..., 20, 30, ...

text
1(-∞, 20] (20, 30] (30, +∞)
2<---------> <--------> <--------> (Next-Key Lock 的逻辑区间)

UPDATE ... WHERE age = 30 会锁定 (20, 30] 这个区间。任何想插入 age 在 21 到 30 之间的新记录的事务都会被阻塞。

常见低分回答

  1. “RR 级别完美解决了幻读。” —— 这个回答过于绝对。虽然 InnoDB 的 RR 在大多数情况下解决了幻读,但在某些极端复杂的并发场景下(例如,一个事务先快照读,然后根据结果去更新,此时可能出现逻辑上的幻读),幻读问题依然可能以其他形式出现。更准确的说法是“在很大程度上避免了幻读”。
  2. “RR 就是通过加锁来解决幻读的。” —— 这个回答过于笼统,没有区分快照读和当前读。它忽略了 MVCC 在快照读场景下的关键作用,只提锁是不完整的。
  3. 混淆不可重复读和幻读。 —— 例如,错误地描述“一个事务 UPDATE 了一条数据,另一个事务看到了,这就是幻读”。这是典型的不可重复读,而非幻读。幻读特指行数的变化。
  4. 只提 MVCC,不提 Next-Key Lock。 —— 这是最常见的半吊子回答。只懂 MVCC 只能解释快照读,无法解释为什么 SELECT ... FOR UPDATE 这样的当前读也能避免幻读。
  5. “所有 SELECT 都会加 Next-Key Lock。” —— 错误。在 RR 级别下,普通的 SELECT 是快照读,不加锁。只有显式指定 FOR UPDATE 或 LOCK IN SHARE MODE 的 SELECT 以及 INSERT/UPDATE/DELETE 才会加锁。

工程实践与优化

  • Gap Lock 的代价:Next-Key Lock 中的 Gap Lock 是解决幻读的关键,但它也降低了并发性。因为它会锁住一个范围,导致其他事务无法在该范围内插入即使不冲突的数据。例如,锁定 age > 25 的间隙会阻止 age = 40 和 age = 50 的插入,尽管它们可能与事务逻辑无关。
  • 如何绕过 Gap Lock:在对并发性能要求极高,且能容忍幻读的场景下,可以将事务隔离级别降为 Read Committed (RC)。RC 级别下,InnoDB 不会使用 Gap Lock,只会使用 Record Lock,从而大大提高并发插入的性能。
  • 索引的重要性:Next-Key Lock 强依赖于索引。如果当前读的 WHERE 条件没有走索引,或者索引是全表扫描,InnoDB 可能会锁住整个表或者聚簇索引中的每个间隙,导致系统并发能力急剧下降,这在生产环境中是灾难性的。
  • 死锁:复杂的 Next-Key Lock 交互是死锁的常见原因。两个事务互相等待对方释放的间隙锁,就会形成死锁。排查死锁需要使用 SHOW ENGINE INNODB STATUS 命令,并仔细分析 LATEST DETECTED DEADLOCK 部分。

常见追问与延伸题目

  1. 追问:既然 InnoDB 的 RR 级别已经很大程度上解决了幻读,为什么还需要 Serializable 隔离级别?

    • 回答要点:
      • 理论完备性:Serializable 提供了 SQL 标准中最高级别的隔离,保证了完全的串行化执行,从理论上杜绝所有并发异常。
      • 解决特殊场景:在 RR 级别下,如果一个事务中混合了快照读和当前读,并且逻辑依赖于快照读的结果,仍然可能出现问题。例如,事务 T1 先 SELECT 检查某个条件(快照读),然后根据结果决定是否 INSERT(当前读)。在这个间隙,T2 可能已经插入了数据并提交,导致 T1 的 INSERT 失败或产生非预期的结果。
      • 实现差异:Serializable 级别会将所有 SELECT 语句隐式地转换为 SELECT ... LOCK IN SHARE MODE,即所有读都变成当前读并加锁,彻底杜绝了快照读和当前读混合带来的不一致性,但代价是并发性能大幅降低。
  2. 追问:在 RR 级别下,如果一个 UPDATE 语句的 WHERE 条件没有使用索引,会发生什么?

    • 回答要点:
      • 全表扫描:MySQL 将不得不进行全表扫描来找到匹配的行。
      • 行锁与表级锁效应:InnoDB 会对扫描过程中遇到的每一行都加上 Next-Key Lock。由于聚簇索引是顺序的,这相当于锁住了表中的所有记录以及所有记录之间的间隙。
      • 灾难性后果:这种操作会阻塞对该表的几乎所有其他 INSERT 操作,以及对任何行的 UPDATE 和 DELETE 操作。其效果接近于锁定了整张表,对并发是毁灭性的打击。这是生产中必须极力避免的情况。
  3. 追问:Read Committed (RC) 和 Repeatable Read (RR) 在 InnoDB 实现上的核心区别是什么?

    • 回答要点:
      • Read View 生成时机:这是最核心的区别。在 RR 级别,Read View 在事务开始时创建一次,整个事务期间都使用这个 Read View。在 RC 级别,每条 SELECT 语句执行前都会重新创建一个新的 Read View。这就是为什么 RC 会读到其他事务已提交的修改(不可重复读)。
      • 锁策略:RR 级别在当前读时使用 Next-Key Lock(Record Lock + Gap Lock)来防止幻读。而 RC 级别,为了更高的并发性,禁用了 Gap Lock,只使用 Record Lock。因此 RC 无法阻止幻读的发生。