问题

InnoDB的RR到底有没有解决幻读?

答案

核心结论

部分解决,但不是完全解决。InnoDB在REPEATABLE READ隔离级别下:

  • 通过MVCC机制,在快照读场景下避免了幻读
  • 通过Next-Key Lock,在当前读场景下基本解决了幻读
  • 但在某些特殊场景下(混合快照读和当前读),仍可能出现幻读现象

1. 快照读场景:已解决幻读

原理

  • 事务第一次SELECT时创建ReadView,后续查询复用同一快照
  • 其他事务新插入的数据,对当前事务不可见

示例

-- 事务A(快照读)
START TRANSACTION;
SELECT * FROM user WHERE age > 20; -- 返回3条
-- 第一次查询时生成ReadView

-- 事务B插入新数据并提交
INSERT INTO user (id, age) VALUES (100, 25);
COMMIT;

-- 事务A再次查询
SELECT * FROM user WHERE age > 20; -- 仍返回3条(未出现幻读)
-- 使用同一ReadView,看不到新插入的数据

COMMIT;

解决机制

通过Undo Log版本链和ReadView可见性判断,新插入数据的trx_id不在当前事务的可见范围内。


2. 当前读场景:基本解决幻读

原理

  • 当前读会加Next-Key Lock(记录锁+间隙锁)
  • 锁住查询范围内的记录和间隙,阻止其他事务插入数据

示例

-- 事务A(当前读)
START TRANSACTION;
SELECT * FROM user WHERE age > 20 FOR UPDATE; -- 返回3条
-- 对查询范围加Next-Key Lock

-- 事务B尝试插入
INSERT INTO user (id, age) VALUES (100, 25);
-- 被阻塞,等待事务A释放锁

-- 事务A再次查询
SELECT * FROM user WHERE age > 20 FOR UPDATE; -- 仍返回3条(未出现幻读)

COMMIT; -- 释放锁,事务B才能继续

锁的范围

假设表中age索引值为:15, 22, 28, 35

SELECT * FROM user WHERE age > 20 FOR UPDATE;

加锁范围:

  • (20, 22]:Next-Key Lock
  • (22, 28]:Next-Key Lock
  • (28, 35]:Next-Key Lock
  • (35, +∞):Gap Lock

任何尝试在age>20范围内插入数据的事务都会被阻塞。


3. 特殊场景:仍可能出现幻读

场景1:先快照读,后当前读

-- 事务A
START TRANSACTION;

-- 第一步:快照读
SELECT * FROM user WHERE age > 20; -- 返回3条
-- 生成ReadView

-- 事务B插入新数据并提交
INSERT INTO user (id, age, name) VALUES (100, 25, 'Bob');
COMMIT;

-- 第二步:当前读(读最新数据)
SELECT * FROM user WHERE age > 20 FOR UPDATE; -- 返回4条(出现幻读!)
-- 当前读会读到最新提交的数据,而不是快照

-- 第三步:快照读
SELECT * FROM user WHERE age > 20; -- 返回3条
-- 仍然使用快照,看不到新数据

COMMIT;

问题分析

  • 快照读使用MVCC,看到的是快照版本(3条)
  • 当前读读取最新数据,看到新插入的记录(4条)
  • 同一事务中,两种读方式看到的数据不一致,产生了”幻读”的效果

场景2:UPDATE导致的幻读

-- 假设表中有:id=1 (age=25), id=2 (age=30)

-- 事务A
START TRANSACTION;
SELECT * FROM user WHERE age > 20; -- 返回2条

-- 事务B插入并提交
INSERT INTO user (id, age, name) VALUES (3, 28, 'Charlie');
COMMIT;

-- 事务A执行UPDATE(当前读操作)
UPDATE user SET name = 'Updated' WHERE age > 20;
-- 影响了3行(包括事务B插入的记录)

-- 事务A再次查询
SELECT * FROM user WHERE age > 20; -- 返回3条(出现幻读!)
-- UPDATE操作将新记录也修改了,导致快照读也能看到

COMMIT;

问题分析

  • UPDATE是当前读操作,会修改所有符合条件的记录(包括新插入的)
  • 修改后,当前事务对该记录的修改可见(因为是自己修改的)
  • 再次快照读时,因为该记录的最新版本是当前事务创建的,所以可见

为什么不能完全解决?

  1. 快照读和当前读的语义不同
    • 快照读:一致性视图(历史版本)
    • 当前读:最新版本(实时数据)
  2. 性能考量
    • 如果所有操作都加锁(SERIALIZABLE),性能会急剧下降
    • InnoDB在一致性和性能之间做了权衡
  3. UPDATE/DELETE的当前读特性
    • 这些操作必须读取最新数据才能正确执行
    • 可能会”意外”看到其他事务新插入的数据

如何彻底避免幻读?

方案1:使用SERIALIZABLE隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT * FROM user WHERE age > 20;
-- 自动加共享锁,完全阻塞其他事务的INSERT

方案2:在RR级别下主动加锁

START TRANSACTION;
-- 对查询范围加锁
SELECT * FROM user WHERE age > 20 FOR UPDATE;
-- 后续操作都不会出现幻读

方案3:统一使用当前读

START TRANSACTION;
-- 全部使用FOR UPDATE
SELECT * FROM user WHERE age > 20 FOR UPDATE;
-- ... 其他操作
SELECT * FROM user WHERE age > 20 FOR UPDATE;
COMMIT;

面试要点总结

  1. InnoDB的RR不是完全解决幻读,而是大部分场景下避免了幻读
  2. 快照读通过MVCC完全避免幻读
  3. 当前读通过Next-Key Lock基本解决幻读
  4. 混合使用快照读和当前读时,可能出现幻读现象
  5. 理解快照一致性实时一致性的区别
  6. SQL标准中的幻读定义InnoDB的实现存在差异
  7. 如果业务要求完全避免幻读,应使用SERIALIZABLE或在RR级别下主动加锁