InnoDB的MVCC机制

InnoDB 的 MVCC 机制详解

MVCC = Multi-Version Concurrency Control,多版本并发控制

典型考点:为什么要有 MVCC?InnoDB 是怎么实现的?和事务隔离级别有什么关系?


一、为什么需要 MVCC?

先看一个典型冲突场景:

  • 事务 A:正在更新一行数据;
  • 事务 B:此时要读这行数据;
  • 如果简单用行锁:
    • 要嘛让 B 等 A 提交(读被阻塞);
    • 要嘛让 B 读到"未提交"的数据(脏读);

在高并发场景下,如果大量读都被锁住,性能会非常惨。

MVCC 的目标:

让"读"和"写"尽量互不阻塞:

  • 读:读到一个"对自己来说一致的历史快照";
  • 写:在自己的版本上改,不影响其他事务看到的历史版本。

一句话:

  • 没有 MVCC:要么锁得很凶,要么读到很脏。
  • 有了 MVCC:大部分"普通查询"可以不用加锁就读到一致性视图。

二、MVCC 在 InnoDB 里能解决什么?

InnoDB 的 MVCC 主要解决两件事:

  1. 提高并发性能:

    • 普通 SELECT(快照读)不用加锁,不会阻塞写;
    • 写操作也只是基于自己的版本链修改。
  2. 实现一致性读(Consistent Read):

    • 在同一个事务里,多次读取同一行数据,在可重复读(RR)下保持结果一致;
    • 即使其他事务已经提交了新值,对当前事务来说仍然不可见。

注意:MVCC 不是单独存在的,它是 InnoDB 在 RC / RR 隔离级别下实现读一致性的重要手段


三、InnoDB 表里的"隐藏列"

InnoDB 每一行数据都悄悄多了两个关键隐藏字段(还有一个 DB_ROW_ID 暂不细讲):

  1. DB_TRX_ID

    • 最近一次修改这一行的事务 ID
    • 插入 / 更新行时会写入当前事务 ID。
  2. 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 = 指向更早版本

每次更新:

  • 并不是直接覆盖旧数据,而是:
    1. 先把旧值写入 Undo Log 形成一个"历史版本";
    2. 更新当前行的 value;
    3. 更新 DB_TRX_ID 为当前事务 ID;
    4. 更新 DB_ROLL_PTR 指向刚刚那条 Undo 记录。

于是同一行就有了一个版本链


四、Undo Log 与版本链

1. Undo Log 的作用

Undo 主要有两个作用:

  1. 回滚事务:

    • 事务失败 / 回滚时,利用 Undo Log 把数据"恢复"到原来的值。
  2. 支持 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 来说:

  1. 如果 DB_TRX_ID 还没开始(大于 Read View 的最大已分配事务 ID)

    → 这个版本来自未来,对当前事务不可见。

  2. 如果 DB_TRX_ID 在当前 Read View 的活跃事务列表中

    → 说明这个修改对应的事务还没提交,对当前事务不可见。

  3. 其他情况:

    • 要么是已经提交的老事务;
    • 要么是当前事务自己;
      对当前事务可见

如果当前版本不可见,就沿着 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 种隔离级别:

  1. READ UNCOMMITTED(读未提交)
  2. READ COMMITTED(读已提交)
  3. REPEATABLE READ(可重复读,默认
  4. 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 的?

关键点:

  1. 每行有隐藏列:DB_TRX_IDDB_ROLL_PTR
  2. 更新时写 Undo Log,形成版本链;
  3. 读取时通过 Read View 判断哪个版本对当前事务可见:
    • 当前版本不可见就顺着 DB_ROLL_PTR 找更旧版本;
  4. 快照读(普通 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。

十、小结

  1. **MVCC 的核心目的:**提升并发性能,让读无需加锁还能保持读一致性。
  2. InnoDB 实现 MVCC 的关键要素:
    • 行记录隐藏列:DB_TRX_IDDB_ROLL_PTR
    • Undo Log 形成历史版本链;
    • Read View 决定当前事务能看到哪个版本。
  3. 快照读 vs 当前读:
    • 快照读:普通 SELECT,用 MVCC,不加锁;
    • 当前读:UPDATE / DELETE / SELECT ... FOR UPDATE,需要加锁。
  4. 和隔离级别关系:
    • RC:每次读新建 Read View;
    • RR:事务内共享 Read View,实现可重复读。

理解了这些,你就可以在面试里从"为什么需要 MVCC → InnoDB 的实现细节 → 和隔离级别的关系"一条线讲下来,非常完整。

相关推荐
MC皮蛋侠客5 小时前
MySQL数据库迁移脚本及使用说明
数据库·mysql
CoderYanger5 小时前
贪心算法:4.摆动序列
java·算法·leetcode·贪心算法·1024程序员节
默 语6 小时前
Spring-AI vs LangChain4J:Java生态的AI框架选型指南
java·人工智能·spring·ai·langchain·langchain4j·spring-ai
kk哥88996 小时前
springboot静态资源的核心映射规则
java·spring boot·后端
老毛肚6 小时前
Java两种代理模式详解
java·开发语言·代理模式
要站在顶端6 小时前
Jenkins PR编号提取&环境变量赋值问题总结
java·servlet·jenkins
愚公移码6 小时前
蓝凌EKP产品:Hibernate 中 SessionFactory、Session 与事务的关系
java·数据库·hibernate·蓝凌
透明的玻璃杯6 小时前
sqlite数据库连接池
jvm·数据库·sqlite
TT哇6 小时前
【每日八股】面经常考
java·面试