面试记录 2026-10-13
题目:InnoDB在RR隔离级别下如何解决幻读?
难度:high
标签:db/mysql/transaction, mysql/mvcc, mysql/gap-lock, difficulty/high
评分:⭐⭐⭐☆☆(3/5)
我的回答
InnoDB 在可重复读下解决幻读使用的是 MVCC,解决了部分幻读,并没有完全解决幻读。
它解决的方式是基于版本控制,当事务开启时,第一次进行 SELECT 的时候,创建对应的事务版本号,然后依据 MVCC 的机制读取事务,根据事务的 ID 去读取当前可读到的事务 ID 信息,来进行读取数据的控制,当进行了数据删除的时候,还是会导致 MVCC 中事务数据出现丢失,发生 SELECT 的返回结果不一致的问题。但是它还是能处理一部分幻读问题的。
点评
优点:
- 意识到 RR 没有完全解决幻读:这是很关键的认知,很多候选人会误以为 RR 完全防幻读
- 提到了 MVCC 是手段之一:抓住了快照读路径
- 能想到边界情况:删除操作会影响数据一致性
不足:
- 完全遗漏了 Gap Lock / Next-Key Lock:这是 RR 解决幻读的另一半机制(针对当前读),比 MVCC 更关键
- MVCC 描述混乱:说法"当进行了数据删除的时候,还是会导致 MVCC 中事务数据出现丢失"表述不准——MVCC 下当前事务本来就看不到别人的修改(包括删除),所以快照读不会"丢失",只是读不到别人的最新值
- 未区分快照读 vs 当前读:RR 防幻读要分两条路径讲:
- 快照读(普通 SELECT):靠 MVCC 完全不出现幻读
- 当前读(SELECT FOR UPDATE / UPDATE / DELETE):靠 Next-Key Lock 防幻读
- 未讲 MVCC 三要素:hidden columns(trx_id、roll_pointer)+ undo 版本链 + ReadView(m_ids、min/max/creator_trx_id)
- 未讲 Next-Key Lock 的构成:Record Lock + Gap Lock 组合
- 未讲"其实还是不能完全防幻读"的具体场景:如 RR 下混用快照读和当前读的边界
补充:
- RR 防幻读双机制:
- 快照读(MVCC):事务内首次 SELECT 建 ReadView 后复用,读的是"首次读时刻的快照",其他人的 INSERT/UPDATE/DELETE 都不可见
- 当前读(Next-Key Lock):
SELECT ... FOR UPDATE / LOCK IN SHARE MODE、UPDATE、DELETE 加 Next-Key Lock(记录锁 + 间隙锁),锁住索引区间,其他事务无法在区间内插入
- InnoDB 的 Next-Key Lock 只在 RR 及以上级别默认加,RC 只加 Record Lock 不加 Gap Lock
- 典型反例场景:
- RC 下 A
SELECT id FROM t WHERE id=5 FOR UPDATE(不防幻读)→ B INSERT (6) → A SELECT ... FOR UPDATE 能看到 6(RC 不防幻读)
- RR 下 A
SELECT id FROM t WHERE id=5 FOR UPDATE(加 Next-Key Lock 锁住间隙)→ B INSERT (6) 被阻塞(RR 防幻读)
- RR 下依然存在的"幻读"变体:
- 混用快照读和当前读:事务 A 先用普通 SELECT 读一次走 ReadView,再用 SELECT FOR UPDATE 当前读,如果其他事务在间隙内插入并提交了,A 的当前读能看到新插入的行——不是 MVCC 的锅,是当前读本身读的是最新已提交数据
- RC vs RR 差异:
- ReadView 创建:RC 每次快照读新建、RR 首次读复用
- 锁:RC 只加 Record Lock、RR 加 Next-Key Lock
完整讲解
第一部分:知识重点剖析
核心考点定位:考察能否讲清 RR 隔离级别下"快照读靠 MVCC、当前读靠 Next-Key Lock"的双机制,并识别出"InnoDB 其实不 100% 防幻读"的边界。
知识点拆解:
- 幻读的定义
- 事务 A 两次相同查询返回结果行数不同——即看到"新插入的行"或"消失的行"
- 幻读 ≠ 脏读(读到未提交)≠ 不可重复读(同一行值变了)
- 隔离级别防幻读能力:RC 不防、RR 大部分防、SERIALIZABLE 完全防
- RR 防幻读的两条路径
- 路径 1:快照读(普通 SELECT)—— 靠 MVCC
- 事务首次一致性读时创建 ReadView
- ReadView 复用,读到的是"首次读时刻"的快照
- 其他事务的 INSERT / UPDATE / DELETE 都不可见
- 效果:快照读路径完全不出现幻读
- 路径 2:当前读(SELECT FOR UPDATE / UPDATE / DELETE)—— 靠 Next-Key Lock
- Next-Key Lock = Record Lock + Gap Lock
- 加锁范围:索引上匹配的记录 + 记录之间的间隙
- 其他事务想在间隙内插入 → 被阻塞,直到持锁事务提交或回滚
- 效果:当前读路径大部分不出现幻读
- MVCC 三要素
- 隐藏列:每行有
trx_id(最近修改的事务 ID)和 roll_pointer(指向 undo 上一版本)
- Undo Log 版本链:每次修改写一条 undo,通过 roll_pointer 串成版本链
- ReadView:事务做快照读时创建,含
m_ids、min_trx_id、max_trx_id、creator_trx_id
- 可见性判断规则:
trx_id < min_trx_id → 可见(事务已提交)
trx_id = max_trx_id → 不可见(还没分配 ID 的事务)
min_trx_id <= trx_id < max_trx_id → 不在 m_ids 中可见(已提交),在 m_ids 中不可见(活跃)
trx_id = creator_trx_id → 自己可见
- RC vs RR 差异:RC 每次快照读新建 ReadView、RR 首次读后复用
- Next-Key Lock 的构成与作用
- Record Lock:锁索引上的具体记录
- Gap Lock:锁索引上的区间(不含记录),比如锁 (id=3, id=7) 之间的区间
- Next-Key Lock:Record + Gap 的组合,锁 (左端区间, 记录]
- Insert Intention Lock:insert 前的特殊 gap lock,多个 insert 可并发
- 锁范围示例:
(a, b, c) 索引,WHERE c=5 FOR UPDATE 若命中 c=5 的记录,会锁住 c=5 的记录 + c=5 之后到下一记录的间隙
- RC 差异:RC 只加 Record Lock,不加 Gap Lock,所以 RC 下间隙内可以并发插入
- RR 不 100% 防幻读的边界
- 官方文档承认:InnoDB 大部分场景防幻读,但不保证完全
- 经典反例场景 1:混用快照读和当前读
- 事务 A 先用普通 SELECT 读一次走 ReadView
- 事务 A 再用 SELECT FOR UPDATE 当前读,如果其他事务在间隙内插入并提交了,A 的当前读能看到新插入的行
- 这不是 MVCC 的锅,是当前读本身读的是最新已提交数据
- 经典反例场景 2:间隙锁的锁范围陷阱
- 假设索引是
(a, b) 联合索引
- 事务 A:
SELECT * FROM t WHERE a=1 AND b=2 FOR UPDATE(加 Next-Key Lock)
- 事务 B:
INSERT INTO t VALUES (1, 2, 100)(走 gap lock 插入,被 A 阻塞)
- 事务 A 提交后 B 插入成功
- 事务 A 再 SELECT FOR UPDATE 能看到 B 插入的行——这是"当前读看到当前读插入",不是幻读
- 核心结论:只要事务内混用快照读和当前读,就可能看到"看似幻读"的现象
- 常见误区辨析
- 误区 1:RR 完全防幻读 —— 错,官方不保证 100%
- 误区 2:MVCC 是 RR 唯一手段 —— 错,只是快照读的手段
- 误区 3:RR 靠行锁防幻读 —— 错,靠的是 Next-Key Lock(含间隙锁)
- 误区 4:RC 也能防幻读 —— 错,RC 只加 Record Lock 不防间隙插入
第二部分:考察方向与延伸递进
考察方向:
- 深度:能否说清 MVCC 和 Next-Key Lock 各自的触发场景
- 广度:能否覆盖 ReadView 可见性规则、锁体系全貌
- 应用:能否识别线上"看似幻读"的真实场景,理解 InnoDB 不完全防幻读的边界
延伸递进面试题:
- RC 和 RR 在锁行为上有哪些差异?
- 解答要点:ReadView 创建时机(RC 每次、RR 首次复用);锁 RR 加 Next-Key Lock、RC 只加 Record Lock;RC 允许间隙插入,RR 阻塞
- RR 下"当前事务能看到自己提交后又看到别的行"是什么情况?
- 解答要点:混用快照读和当前读。快照读走 ReadView 不看新插入,当前读会看到已提交的插入
- 如何在不影响业务的前提下测试 RR 是否真的防幻读?
- 解答要点:两个会话,会话 A
BEGIN; SELECT id FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE;(不 commit);会话 B INSERT (15) 被阻塞;A commit;B 提交成功;A 再 SELECT FOR UPDATE 才看得到 15——这是"当前读看到当前读插入",不是幻读;改成快照读 A 始终看不到 15
第三部分:面试者参考回答(口语化)
面试官您好,InnoDB 在 RR 隔离级别下解决幻读我用双机制来答,同时也要说清它并不 100% 防。
第一个机制是快照读靠 MVCC。普通 SELECT 是快照读,事务做第一次快照读时会创建 ReadView,含 m_ids、min_trx_id、max_trx_id、creator_trx_id 四个字段,用这套规则判断当前行版本是否可见。RR 的关键是首次读后 ReadView 复用——不像 RC 每次都新建。所以事务后续再读,还是那个快照时刻的数据,其他事务的 INSERT、UPDATE、DELETE 都看不到。快照读这条路径完全不出现幻读。
第二个机制是当前读靠 Next-Key Lock。SELECT FOR UPDATE、UPDATE、DELETE 都是当前读,会加 Next-Key Lock,也就是 Record Lock + Gap Lock 的组合。Gap Lock 锁的是索引上的间隙区间(不含记录),比如锁 (id=3, id=7) 之间的间隙。其他事务想在这个间隙内插入,会被阻塞直到持锁事务提交或回滚。RR 默认加 Next-Key Lock,RC 只加 Record Lock 不加 Gap Lock——所以 RC 不防间隙插入导致的幻读,RR 大部分能防。
但要承认 InnoDB 并不 100% 防幻读,官方文档也是这么说的。典型反例是事务内混用快照读和当前读:事务 A 先用普通 SELECT 读一次走 ReadView,再用 SELECT FOR UPDATE 当前读,如果其他事务在间隙内插入并提交了,A 的当前读就能看到这个新插入的行——这不是 MVCC 的锅,是当前读本身读的是最新已提交数据。另外还有一些间隙锁范围陷阱,比如锁的是索引列的间隙而不是主键的间隙,也会让"看似幻读"的现象出现。
再补充个 RC vs RR 的差异。RC 每次快照读都新建 ReadView、RR 首次读后复用;RC 只加 Record Lock、RR 加 Next-Key Lock。所以 RC 完全不防幻读,RR 大部分防但不完美。生产上 Spring Boot 默认其实是 RC,MySQL 官方默认是 RR,两者选择看业务——读多写少、要防幻读选 RR;写多、有间隙锁性能顾虑选 RC。
当然,如果真的想 100% 防幻读,只能上 SERIALIZABLE 隔离级别,但代价是所有读都加 S 锁,串行化执行,业务基本会崩。所以工程上都是接受 RR 的"大部分防"这个折中。