问题
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是当前读操作,会修改所有符合条件的记录(包括新插入的)
- 修改后,当前事务对该记录的修改可见(因为是自己修改的)
- 再次快照读时,因为该记录的最新版本是当前事务创建的,所以可见
为什么不能完全解决?
- 快照读和当前读的语义不同
- 快照读:一致性视图(历史版本)
- 当前读:最新版本(实时数据)
- 性能考量
- 如果所有操作都加锁(SERIALIZABLE),性能会急剧下降
- InnoDB在一致性和性能之间做了权衡
- 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;
面试要点总结
- InnoDB的RR不是完全解决幻读,而是大部分场景下避免了幻读
- 快照读通过MVCC完全避免幻读
- 当前读通过Next-Key Lock基本解决幻读
- 混合使用快照读和当前读时,可能出现幻读现象
- 理解快照一致性和实时一致性的区别
- SQL标准中的幻读定义和InnoDB的实现存在差异
- 如果业务要求完全避免幻读,应使用SERIALIZABLE或在RR级别下主动加锁