InnoDB 的 MVCC 机制详解
MVCC = Multi-Version Concurrency Control,多版本并发控制
典型考点:为什么要有 MVCC?InnoDB 是怎么实现的?和事务隔离级别有什么关系?
一、为什么需要 MVCC?
先看一个典型冲突场景:
- 事务 A:正在更新一行数据;
- 事务 B:此时要读这行数据;
- 如果简单用行锁:
- 要嘛让 B 等 A 提交(读被阻塞);
- 要嘛让 B 读到"未提交"的数据(脏读);
在高并发场景下,如果大量读都被锁住,性能会非常惨。
MVCC 的目标:
让"读"和"写"尽量互不阻塞:
- 读:读到一个"对自己来说一致的历史快照";
- 写:在自己的版本上改,不影响其他事务看到的历史版本。
一句话:
- 没有 MVCC:要么锁得很凶,要么读到很脏。
- 有了 MVCC:大部分"普通查询"可以不用加锁就读到一致性视图。
二、MVCC 在 InnoDB 里能解决什么?
InnoDB 的 MVCC 主要解决两件事:
-
提高并发性能:
- 普通
SELECT(快照读)不用加锁,不会阻塞写; - 写操作也只是基于自己的版本链修改。
- 普通
-
实现一致性读(Consistent Read):
- 在同一个事务里,多次读取同一行数据,在可重复读(RR)下保持结果一致;
- 即使其他事务已经提交了新值,对当前事务来说仍然不可见。
注意:MVCC 不是单独存在的,它是 InnoDB 在 RC / RR 隔离级别下实现读一致性的重要手段。
三、InnoDB 表里的"隐藏列"
InnoDB 每一行数据都悄悄多了两个关键隐藏字段(还有一个 DB_ROW_ID 暂不细讲):
-
DB_TRX_ID:- 最近一次修改这一行的事务 ID;
- 插入 / 更新行时会写入当前事务 ID。
-
DB_ROLL_PTR(回滚指针):- 指向 Undo Log(撤销日志)中的记录;
- 通过它可以找到该行的旧版本 ,形成版本链。
可以想象为:
text
当前行:
value = 100
DB_TRX_ID = 80
DB_ROLL_PTR = 指向上一个版本
Undo Log 里:
上一个版本:
value = 80
DB_TRX_ID = 60
DB_ROLL_PTR = 指向更早版本
每次更新:
- 并不是直接覆盖旧数据,而是:
- 先把旧值写入 Undo Log 形成一个"历史版本";
- 更新当前行的 value;
- 更新
DB_TRX_ID为当前事务 ID; - 更新
DB_ROLL_PTR指向刚刚那条 Undo 记录。
于是同一行就有了一个版本链。
四、Undo Log 与版本链
1. Undo Log 的作用
Undo 主要有两个作用:
-
回滚事务:
- 事务失败 / 回滚时,利用 Undo Log 把数据"恢复"到原来的值。
-
支持 MVCC:
- 对于已经提交或正在进行的其他事务,
可以通过 Undo Log 找到对它们来说可见的历史版本。
- 对于已经提交或正在进行的其他事务,
2. 版本链示意图
假设有一行数据被多次更新:
text
最新版本在聚簇索引页中:
[当前行]
id = 1
value = 300
DB_TRX_ID = 90
DB_ROLL_PTR -> Undo#3
Undo 链:
Undo#3:
old value = 200
DB_TRX_ID = 70
DB_ROLL_PTR -> Undo#2
Undo#2:
old value = 150
DB_TRX_ID = 60
DB_ROLL_PTR -> Undo#1
Undo#1:
old value = 100
DB_TRX_ID = 50
DB_ROLL_PTR -> null
当不同事务来读的时候,InnoDB 会:
- 拿着当前行,从
DB_TRX_ID和当前事务的Read View(后面讲)比较; - 如果该版本不可见,就顺着
DB_ROLL_PTR去 Undo 里找更老的版本,直到找到一个对当前事务可见的版本。
五、Read View:MVCC 的"视图控制器"
Read View 是 MVCC 的灵魂。
它里面大致记录了:
- 当前系统中还活跃(未提交)的事务 ID 列表(或区间);
- 当前创建 Read View 的事务 ID;
- 已经分配的最大事务 ID 等信息。
1. 可见性判断规则(核心思路)
当一个事务 T 读取一行记录时,InnoDB 会根据 Read View 判断:
当前版本(或历史某个版本)是否对 T 可见。
简化版规则(直觉就够):
对某个版本的 DB_TRX_ID 来说:
-
如果
DB_TRX_ID还没开始(大于 Read View 的最大已分配事务 ID)→ 这个版本来自未来,对当前事务不可见。
-
如果
DB_TRX_ID在当前 Read View 的活跃事务列表中→ 说明这个修改对应的事务还没提交,对当前事务不可见。
-
其他情况:
- 要么是已经提交的老事务;
- 要么是当前事务自己;
→ 对当前事务可见。
如果当前版本不可见,就沿着 DB_ROLL_PTR 去 Undo 找上一个版本,再执行同样的判断,直到找到一个可见版本或者链结束。
2. Read View 在什么时候创建?
这和隔离级别有关系(重点:RC / RR)。
-
在 读已提交(READ COMMITTED) 下:
- 每一次快照读都会新生成一个 Read View;
- 因此同一条 SQL 多次执行可能看到不同结果。
-
在 可重复读(REPEATABLE READ,InnoDB 默认) 下:
- 一个事务第一次快照读时创建 Read View;
- 同一事务后续的快照读复用这个 Read View;
- 所以在事务期间,同一行的快照读结果保持一致。
这就是为什么:
- RR 下:可以做到"可重复读";
- RC 下:每次读看到的是"最新已提交"的版本。
六、快照读 vs 当前读
MVCC 在 InnoDB 中只对**快照读(一致性读)**生效。
1. 快照读(Snapshot Read / Consistent Read)
典型形式:
sql
SELECT * FROM t WHERE id = 1;
特点:
- 不加锁(不显式 FOR UPDATE / LOCK IN SHARE MODE);
- 返回的是:根据 MVCC + Read View 算出来的某个历史版本;
- 不会阻塞写事务,也不会被写阻塞。
2. 当前读(Current Read)
典型形式:
sql
SELECT ... FOR UPDATE;
SELECT ... LOCK IN SHARE MODE;
UPDATE t SET ... WHERE ...;
DELETE FROM t WHERE ...;
INSERT INTO t VALUES (...);
特点:
- 当前读要读取的是最新版本,并且要保证之后能安全修改;
- 通常会加行锁 / 间隙锁等;
- 用于实现当前读 + 串行化修改的语义。
重点:
- MVCC + 快照读:解决"并发读"的性能和一致性问题;
- 锁 + 当前读:解决"并发写 / 并发修改"的安全问题。
七、MVCC 与事务隔离级别的关系
InnoDB 支持的 4 种隔离级别:
- READ UNCOMMITTED(读未提交)
- READ COMMITTED(读已提交)
- REPEATABLE READ(可重复读,默认)
- SERIALIZABLE(可串行化)
1. READ UNCOMMITTED
- 几乎不使用 MVCC,一些实现中连 Undo 可见性都不严格限制;
- 可以读到未提交数据(脏读);
- 不推荐在正常业务中使用。
2. READ COMMITTED(RC)
- 每次快照读(SELECT)都会创建新的 Read View;
- 因此能读到别的事务已经提交的最新数据;
- 可能出现:
- 不可重复读(同一行第 1 次读和第 2 次读结果不同);
- 幻读(范围内多出/少了行)。
RC 下的 MVCC:
- 解决了脏读问题;
- 提供"读已提交的最新快照"。
3. REPEATABLE READ(RR,InnoDB 默认)
- 一个事务第一次快照读时生成 Read View,后续都复用;
- 因此同一行在同一事务中的多次 SELECT 结果保持一致;
- 避免了不可重复读。
至于 幻读:
- 理论上 MVCC 只能解决一部分问题;
- InnoDB 在 RR 下还会使用间隙锁(Gap Lock)、Next-Key Lock 等配合,
实现逻辑上的"防幻读"。
RR + MVCC + 间隙锁,是 InnoDB 的核心实现。
4. SERIALIZABLE
- 所有读都相当于加锁(
SELECT会退化为加共享锁的当前读); - 并发度极低,一般只有在金融等极高一致性要求的场景才会考虑。
八、MVCC 能防什么、不能防什么?
1. 能防:
- 脏读(搭配 RC / RR);
- 不可重复读(在 RR 下);
- 在 RR 下配合锁机制,可以避免大部分"幻读"问题。
2. 不能防:
- MVCC 本身不能完全解决幻读,需要锁机制(间隙锁等)协作;
- 同一事务中执行"当前读"(比如 SELECT ... FOR UPDATE)时,
看到的是最新版本,不再是 MVCC 的快照。
九、面试高频问答小抄
Q1:MVCC 是什么?
多版本并发控制,通过为每行数据维护多个版本(Undo Log + 隐藏列),
在不同事务中为快照读提供一致性视图,从而在读多写多的环境中减少锁冲突。
Q2:InnoDB 是如何实现 MVCC 的?
关键点:
- 每行有隐藏列:
DB_TRX_ID、DB_ROLL_PTR; - 更新时写 Undo Log,形成版本链;
- 读取时通过 Read View 判断哪个版本对当前事务可见:
- 当前版本不可见就顺着
DB_ROLL_PTR找更旧版本;
- 当前版本不可见就顺着
- 快照读(普通 SELECT)使用 MVCC;当前读(UPDATE / SELECT ... FOR UPDATE)依然加锁。
Q3:MVCC 在 InnoDB 中在哪些隔离级别下起作用?
- 主要在:READ COMMITTED、REPEATABLE READ 下起作用;
- RU 几乎不严格走 MVCC;
- SERIALIZABLE 以加锁读为主。
Q4:RC 和 RR 下的 MVCC 有什么区别?
- RC:每次 SELECT 都生成新的 Read View → 能读到最新已提交数据;
- RR:一个事务里首次 SELECT 生成 Read View,后续 SELECT 共享同一个 → 实现可重复读。
Q5:MVCC 和锁的关系?
- 快照读使用 MVCC,不加锁;
- 当前读必须加锁,保证修改安全;
- RR 下的"防幻读"依赖:MVCC + 间隙锁 / Next-Key Lock。
十、小结
- **MVCC 的核心目的:**提升并发性能,让读无需加锁还能保持读一致性。
- InnoDB 实现 MVCC 的关键要素:
- 行记录隐藏列:
DB_TRX_ID、DB_ROLL_PTR; - Undo Log 形成历史版本链;
- Read View 决定当前事务能看到哪个版本。
- 行记录隐藏列:
- 快照读 vs 当前读:
- 快照读:普通 SELECT,用 MVCC,不加锁;
- 当前读:UPDATE / DELETE / SELECT ... FOR UPDATE,需要加锁。
- 和隔离级别关系:
- RC:每次读新建 Read View;
- RR:事务内共享 Read View,实现可重复读。
理解了这些,你就可以在面试里从"为什么需要 MVCC → InnoDB 的实现细节 → 和隔离级别的关系"一条线讲下来,非常完整。