InnoDB 在 可重复读(RR)隔离级别下,是如何解决幻读问题的?
在 MySQL 的 InnoDB 存储引擎中,可重复读(Repeatable Read, 简称 RR)隔离级别下解决幻读问题,并不是靠单一机制完成的,而是根据不同的读取方式(快照读 vs 当前读),采用了两种不同的机制。
简单来说,核心结论是:
- 针对 快照读(普通 SELECT),通过 MVCC(多版本并发控制) 解决。
- 针对 当前读(加锁的 SELECT、UPDATE、DELETE),通过 Next-Key Lock(临键锁 = 记录锁 + 间隙锁) 解决。
以下是详细的原理解析:
1. 什么是幻读?
幻读是指:事务 A 按照某个条件查询出一些记录,此时事务 B 插入(Insert) 了一条符合该条件的新记录并提交。事务 A 再次以相同的条件进行查询时,发现多出了原本不存在的“幻影”记录。
2. 快照读(Snapshot Read)下的解决方案:MVCC
快照读指的是不加锁的普通查询,例如:SELECT * FROM table WHERE ...。
在 RR 隔离级别下,InnoDB 使用 MVCC(多版本并发控制) 来防止幻读:
- Read View(读视图)的生成时机:在 RR 级别下,事务在第一次执行普通的 SELECT 语句时,会生成一个 Read View。整个事务期间,所有的普通 SELECT 都会复用这同一个 Read View。
- 版本可见性规则:Read View 记录了当前系统中活跃的事务 ID。当事务读取数据时,会根据记录中的隐藏字段(事务 ID)与 Read View 进行比对。
- 解决幻读的过程:
如果事务 A 执行了第一次查询,生成了 Read View。随后事务 B 插入了一条新数据并提交。当事务 A 再次执行相同的查询时,由于新插入的数据的事务 ID 比事务 A 的 Read View 中的上限还要新,根据 MVCC 规则,这条新数据对事务 A 是不可见的。因此,事务 A 两次查询的结果一致,没有发生幻读。
3. 当前读(Current Read)下的解决方案:Next-Key Lock
当前读指的是读取最新版本数据并且加锁的操作,例如:
SELECT * FROM table WHERE ... FOR UPDATESELECT * FROM table WHERE ... LOCK IN SHARE MODEUPDATE .../DELETE .../INSERT ...
在当前读下,MVCC 无法阻止其他事务插入数据。为了防止幻读,InnoDB 引入了 Next-Key Lock(临键锁)。
Next-Key Lock = Record Lock(记录锁) + Gap Lock(间隙锁)
- 记录锁:锁住具体的索引记录。
- 间隙锁:锁住两个索引记录之间的间隙,防止其他事务在这个间隙里插入新数据。
解决幻读的过程:
假设表中有 id 为 5, 10, 15 的记录。
事务 A 执行:SELECT * FROM table WHERE id > 8 FOR UPDATE;
此时,InnoDB 不仅会给 id=10 和 id=15 的记录加上记录锁,还会给 (5, 10)、(10, 15) 以及 (15, +∞) 的区间加上间隙锁。
如果此时事务 B 想要插入一条 id=12 的新记录,会被间隙锁阻塞,直到事务 A 提交。因为事务 B 根本无法插入新数据,所以事务 A 再次查询时,也就不会看到所谓的“幻影”记录。
4. 进阶探讨:InnoDB 的 RR 彻底解决幻读了吗?
这是一个高频面试题。答案是:没有完全解决,在特定场景下仍然会发生幻读。
由于快照读和当前读的机制不同,如果在一个事务中混合使用这两种读,或者对“幻影”记录进行了修改,就会触发幻读。
典型的幻读发生场景:
场景一:先快照读,后当前读
- 事务 A 执行普通
SELECT查询 id=5 的记录,发现不存在(快照读,生成 Read View)。 - 事务 B 插入一条 id=5 的记录,并提交。
- 事务 A 执行
SELECT * FROM table WHERE id=5 FOR UPDATE(变为当前读)。此时会去读取最新版本,结果查到了 id=5 的记录。发生幻读。
场景二:更新了“幻影”记录
- 事务 A 执行普通
SELECT查询 id=5 的记录,发现不存在。 - 事务 B 插入一条 id=5 的记录,并提交。
- 事务 A 执行
UPDATE table SET name = 'test' WHERE id = 5;(UPDATE 是当前读,且修改成功,因为 B 已经提交)。 - 由于事务 A 修改了这条记录,这条记录的事务 ID 变成了事务 A 的 ID。
- 事务 A 再次执行普通
SELECT查询,根据 MVCC 规则,自己修改的数据对自己是可见的,于是查出了 id=5 的记录。发生幻读。
总结
- 普通查询(快照读):靠 MVCC 解决幻读。
- 加锁查询/修改(当前读):靠 Next-Key Lock(间隙锁+记录锁) 解决幻读。
- 注意点:在同一个事务中,如果从快照读切换到当前读,或者去修改了其他事务插入的不可见记录,依然会产生幻读现象。要绝对避免幻读,可以在事务一开始就使用
FOR UPDATE加锁(强制走当前读和间隙锁),或者将隔离级别提升到SERIALIZABLE(串行化)。