🧨 先一句话总纲:
RC 每次 SELECT 都重新生成 ReadView(看最新已提交)
RR 一个事务只生成一次 ReadView(看固定快照)
区别就这一个点。
所有连锁反应,包括“不可重复读”怎么来的,都从这里推出来,简单清晰。
🔥 1. MVCC 是怎么实现的(版本链动画)
InnoDB 在每行加两个隐藏字段:
trx_id # 最新更新这行的事务ID
roll_pointer # 指向 undo log(老版本记录)
用这些 Undo 一条条串起来,就形成 “版本链”:
最新版本 → 上一版本 → 再上一版本 → ...trx_id=20 trx_id=15 trx_id=8
🎬 2. ReadView 是 MVCC 的灵魂
ReadView 本质是一张“我能看到哪些事务的提交”的白名单。
包含 4 个关键字段:
m_ids:当前未提交事务ID集合
min_trx_id
max_trx_id
creator_trx_id
你不需要死记这些字段,只要知道:
关键逻辑:
当一个事务在读一行时,InnoDB 会从版本链里往上找:
找第一个 “我有权限读取” 的版本
权限判断规则非常简单:
- 版本的 trx_id 已提交 → ✔ 可读
- 版本的 trx_id 未提交 → ❌ 跳过
- 版本的 trx_id 是自己事务 → ✔ 可读
🧨 3. RC 与 RR 差异:关键是 ReadView 何时生成
这个点你一定要牢牢抓住:
⭐ RC(Read Committed)
每次 SELECT 都重新生成 ReadView
意味着:
同一个事务中的两次 SELECT 看到的数据可能不一样
这导致:
- 不可重复读(Non-repeatable read)
⭐ RR(Repeatable Read)
整个事务只在第一次 SELECT 时生成 ReadView
之后所有 SELECT 都用第一次的 ReadView。
意味着:
同一个事务的所有查询看到的都是“同一个快照”
这确保:
- 可重复读(Repeatable read)
👇 这个动画能让你瞬间懂区别
假设有一行记录:
id=1, age=18
你开启事务 T1:
SET tx_isolation='READ-COMMITTED'; -- RC
BEGIN;
SELECT age FROM user WHERE id=1;
这时 ReadView1 = [记录当前活跃事务]
这时另一个事务 T2 来更新并提交:
UPDATE user SET age=20 WHERE id=1;
COMMIT;
⭐ 现在 T1 再查一次(RC 下):
SELECT age FROM user WHERE id=1;
因为 RC 每次 SELECT 都生成新的 ReadView2,所以:
第一次看到年龄=18
第二次看到年龄=20
就是“不一致”,这就是 不可重复读。
⭐ 那在 RR 下会怎样?
RR 在事务开始/第一次 SELECT 时创建 ReadView,之后都不会变。
即使 T2 已经提交,T1 仍然只能看到旧版本:
第一次看到 18
第二次仍然看到 18
第三次也看到 18
这就是 Repeatable Read。
🧩 所以 RC 与 RR 的本质差异一句话总结:
RC:每次读都看“最新提交的版本”
RR:整个事务都看“自己第一次看见的快照”
🧠 为什么 RR 能解决不可重复读?
因为:
- ReadView 不变 → 你读的范围不变 → 看不到别人提交的更新
🔥 那 RR 能防幻读吗?(经典误区)
答案:InnoDB 的 RR + 间隙锁 = 可以防大部分幻读,但靠的是锁,不是 MVCC。
MVCC 本身不能防幻读。
原因很简单:
MVCC 只解决旧版本的问题
幻读是新插入了一行,这行根本没有旧版本
所以 InnoDB 使用 Gap Lock(间隙锁)来解决这个问题。
例如:
SELECT * FROM user WHERE age BETWEEN 10 AND 30 FOR UPDATE;
会锁住对应范围的间隙,别人无法插入 age 在这个区间的新行。
🧨 核心结论图(你面试直接说这个就够了)
RC RR
ReadView 生成 每次 SELECT 第一次 SELECT
可重复读 ❌ 不保证 ✔ 保证
不可重复读 ✔ 存在 ❌ 避免
幻读 ✔ 存在 ✔ 基本避免(靠锁)
MVCC 使用方式 读旧版本 读固定快照