essay

关于MVCC

#mysql

MySQL InnoDB MVCC 笔记

1. 什么是 MVCC

MVCC 是 Multi-Version Concurrency Control,中文通常叫 多版本并发控制

它不是一种隔离级别,而是数据库为了实现事务隔离和提高并发性能所采用的一种机制。

MVCC 的核心思想很简单:

  • 一条记录在逻辑上可以存在多个版本
  • 读事务不一定读取最新版本
  • 而是读取对自己可见的那个版本

可以先记一句最重要的话:

MVCC 的本质是:写生成新版本,读按可见性规则读取旧版本或当前版本。


2. 为什么需要 MVCC

如果数据库只有锁机制,没有 MVCC,那么并发场景下会出现很多阻塞:

  • 事务 A 正在读,事务 B 想写,可能需要等待
  • 事务 B 正在写,事务 A 想读,也可能需要等待

这样会导致吞吐下降,尤其是在读多写少的业务里表现更明显。

MVCC 的目标就是:

  • 尽量让普通读不加锁
  • 让读写尽量并发执行
  • 在保证隔离性的同时提高系统性能

所以 MVCC 的价值主要有两点:

  1. 提升并发性能
  2. 支持一致性读

3. MVCC 解决了什么问题

MVCC 主要用来解决:

  • 脏读
  • 不可重复读的一部分问题
  • 读写冲突过多的问题

但要注意,MVCC 不是万能的,它并不能独立解决所有并发问题,比如:

  • 写写冲突仍然需要锁来控制
  • 幻读是否完全避免,取决于数据库实现和具体读类型
  • Serializable 级别通常不能只靠 MVCC

4. InnoDB 里 MVCC 的三个核心

如果只看 MySQL InnoDB,MVCC 最核心的三个概念是:

  • trx_id
  • undo log
  • Read View

可以先用一句话概括它们的职责:

  • trx_id:这个版本是谁改的
  • undo log:当前版本不可见时,去哪里找旧版本
  • Read View:当前事务能看见哪些版本

5. trx_id:记录版本的修改者

在 InnoDB 中,一条聚簇索引记录里除了业务字段,还会带一些隐藏字段,其中和 MVCC 关系最紧密的是:

  • trx_id
  • roll_pointer

其中:

  • trx_id 表示最后一次修改这条记录的事务 ID
  • roll_pointer 用来指向这条记录对应的 undo log

例如,一条记录原本是:

id=1, balance=100

如果事务 T10 把它改成:

id=1, balance=200

那么这条最新记录里可以粗略理解为:

balance=200, trx_id=10

如果之后事务 T20 再把它改成 300,最新版本大致就变成:

balance=300, trx_id=20

所以 trx_id 解决的是一个最基础的问题:

当前这个版本,是哪个事务写出来的。


6. undo log:保存旧版本

undo log 是 MVCC 的历史版本基础。

很多人刚学事务时容易误以为:

  • 更新操作就是直接覆盖旧值
  • 数据库自己会“记住以前的值”

实际上不是这样。旧版本之所以还能被读到,是因为 InnoDB 在更新时会把旧值信息写入 undo log

undo log 有两个主要作用:

  1. 事务回滚
  2. MVCC 快照读

6.1 用于事务回滚

如果一个事务执行到一半失败了,或者用户主动执行 ROLLBACK,数据库就需要撤销已经做过的修改。

这时就会利用 undo log 把数据恢复回去。

6.2 用于快照读

如果当前记录版本对某个事务不可见,InnoDB 就会沿着 undo log 去找更早的版本,直到找到一个可见版本。

也就是说:

当前行保存的是最新值,历史值依靠 undo log 回溯。


7. 版本链是怎么形成的

假设原始数据为:

id=1, balance=100

事务 T10 更新为 200,事务 T20 更新为 300

那么逻辑上的版本链可以理解为:

当前版本: balance=300, trx_id=20
    ->
旧版本: balance=200, trx_id=10
    ->
更旧版本: balance=100

这里的“指向旧版本”,底层通常是通过记录中的 roll_pointer 找到对应的 undo log

所以版本链不是把所有版本并排放在数据页里,而是:

  • 数据页里保留最新版本
  • 旧版本通过 undo log 串起来

8. Read View:可见性判断规则

Read View 是 InnoDB 实现一致性读时最关键的东西。

它解决的问题是:

对于当前事务来说,这个版本能不能看见?

数据库并发执行时,系统里同时会存在很多事务:

  • 有的已经提交
  • 有的还没提交
  • 有的比当前事务早开始
  • 有的比当前事务晚开始

不是所有版本都应该被当前事务看到。
因此在快照读时,InnoDB 会构造一个 Read View,它本质上是一套“可见性判断依据”。


9. Read View 中的重要内容

理解原理时,通常记住下面几个字段就够了:

  • creator_trx_id:创建这个 Read View 的事务 ID
  • m_ids:创建 Read View 时,系统中活跃事务 ID 的集合
  • min_trx_id:活跃事务中的最小事务 ID
  • max_trx_id:创建 Read View 时,系统即将分配的下一个事务 ID

它们可以这样理解:

  • m_ids:当前还没结束的事务有哪些
  • min_trx_id:这些活跃事务中最早的是谁
  • max_trx_id:比它更大的事务,说明是在快照创建之后才开始的

10. 可见性判断规则

假设当前要读取的某个版本,它的 trx_id = X

InnoDB 会根据 Read View 判断是否可见,常见规则可以概括为:

10.1 X < min_trx_id

说明这个版本对应的事务,在当前快照创建前就已经提交了。

结果:

  • 可见

10.2 X >= max_trx_id

说明这个事务是在当前快照创建之后才开始的。

结果:

  • 不可见

10.3 min_trx_id <= X < max_trx_id

这时需要看 X 是否在 m_ids 里。

如果 Xm_ids 中,说明:

  • 当前快照创建时,这个事务还没有提交

结果:

  • 不可见

如果 X 不在 m_ids 中,说明:

  • 它虽然落在这个区间内,但在快照创建前已经提交

结果:

  • 可见

10.4 X == creator_trx_id

如果这个版本是当前事务自己写出来的:

  • 可见

否则事务自己改完的数据自己都看不到,显然不合理。


11. 三者如何协同工作

trx_idundo logRead View 配合起来,才构成完整的 MVCC。

一次快照读的大致流程如下:

  1. 先拿到记录的当前版本
  2. 查看当前版本的 trx_id
  3. 根据当前事务的 Read View 判断这个版本是否可见
  4. 如果可见,直接返回
  5. 如果不可见,就通过 roll_pointer 去找对应的 undo log
  6. 回溯到更老的版本
  7. 重复判断,直到找到第一个可见版本

一句话总结这个流程:

当前版本先看 trx_id,不可见就顺着 undo log 回溯,可见性由 Read View 决定。


12. 一个完整例子

假设表中初始数据为:

id=1, balance=100

然后发生如下事务:

12.1 事务 T10

UPDATE account SET balance = 200 WHERE id = 1;

这时最新版本大致变成:

balance=200, trx_id=10

同时旧值 100 被写入 undo log

12.2 事务 T20

UPDATE account SET balance = 300 WHERE id = 1;

这时最新版本变成:

balance=300, trx_id=20

同时旧值 200 写入 undo log,并串到前一个旧版本上。

12.3 事务 T15 发起快照读

假设 T15 创建 Read View 时:

  • T10 已经提交
  • T20 还未提交

那么 T15 读这条记录时会这样判断:

  1. 先看当前版本 300,其 trx_id=20
  2. 发现 T20 还活跃,因此不可见
  3. 顺着 undo log 找到旧版本 200
  4. 发现 T10 已提交,因此可见
  5. 返回 200

所以虽然数据库最新值是 300,但对于事务 T15 而言,它看到的是 200

这就是 MVCC 的实际效果。


13. MVCC 主要服务于快照读

并不是所有读取都走 MVCC。

MVCC 主要用于快照读,典型就是普通的:

SELECT * FROM t WHERE id = 1;

这类读通常不加锁,而是通过 Read View + undo log 返回一个对当前事务可见的版本。

而下面这些操作属于当前读

SELECT * FROM t WHERE id = 1 FOR UPDATE;
SELECT * FROM t WHERE id = 1 LOCK IN SHARE MODE;
UPDATE t SET ...
DELETE FROM t WHERE ...

当前读的特点是:

  • 读取最新版本
  • 通常需要加锁
  • 目的不是读历史快照,而是要对当前数据进行控制

因此:

MVCC 主要解决的是普通 SELECT 的一致性读问题,不是所有 SQL 都靠 MVCC。


14. MVCC 与隔离级别的关系

MVCC 不是隔离级别,它是实现隔离级别的机制之一。

在 InnoDB 中,MVCC 主要服务于:

  • Read Committed
  • Repeatable Read

14.1 Read Committed

特点:

  • 每次快照读都会生成新的 Read View

这意味着:

  • 同一个事务里两次 SELECT
  • 可能看到不同结果

所以它避免了脏读,但不能避免不可重复读。

14.2 Repeatable Read

特点:

  • 通常第一次快照读生成 Read View
  • 整个事务期间复用这个 Read View

这意味着:

  • 同一个事务里多次读同一行
  • 结果保持一致

所以它可以避免不可重复读。

14.3 核心区别

Read CommittedRepeatable Read 的根本区别,不在于有没有 MVCC,而在于:

Read View 的生成与复用时机不同。


15. 为什么 MVCC 能提升并发

MVCC 提升并发的关键原因是:

  • 写事务生成新版本
  • 读事务读旧版本

这样在很多场景下:

  • 读不需要等写
  • 写也不需要因为普通读而阻塞

这比单纯依赖锁的机制有更好的并发性,尤其适合读操作很多的系统。


16. MVCC 不是没有锁

这是一个很常见的误区。

MVCC 的作用是减少读写冲突,不是完全替代锁。

数据库中依然需要各种锁来处理其他问题,例如:

  • 行锁
  • 间隙锁
  • next-key lock
  • 表锁

特别是下面这些场景,单靠 MVCC 不够:

  • 两个事务同时修改同一行
  • 当前读需要锁住最新记录
  • 范围更新、范围删除
  • 更强的隔离级别要求

所以正确理解应该是:

MVCC 和锁是配合关系,不是替代关系。


17. 长事务为什么会带来问题

MVCC 依赖历史版本,而历史版本通常来自 undo log

如果一个事务长时间不结束,那么数据库为了让它继续看到旧快照,就不能过早清理相关历史版本。

这会带来几个问题:

  • undo log 占用变大
  • 版本链变长
  • 快照读回溯成本增加
  • 清理压力上升

所以工程实践里通常都会强调:

事务要尽量短,小,快。

长事务会显著增加 MVCC 的维护成本。


18. 常见误区

18.1 MVCC 就是隔离级别

错误。

MVCC 是实现隔离级别的机制,不是隔离级别本身。

18.2 有了 MVCC 就完全不需要锁

错误。

MVCC 主要解决快照读问题,写写冲突和当前读仍然要依赖锁。

18.3 普通 SELECT 读的一定是最新值

错误。

普通 SELECT 在 MVCC 下读到的是对当前事务可见的版本,不一定是数据库中最新版本。

18.4 MVCC 可以解决所有幻读问题

不严谨。

幻读是否避免,取决于数据库实现细节、隔离级别以及是快照读还是当前读,不能简单一句话概括成“完全解决”。


19. 一句话总结 MVCC

如果只用一句话总结:

MVCC 就是通过“多版本 + 可见性判断”来实现一致性读,让普通读写尽量不互相阻塞。

如果再具体一点:

InnoDB 通过记录中的 trx_id、历史版本 undo log、以及事务的 Read View,决定当前事务到底应该看到哪个版本的数据。


20. 面试版总结

如果要用一段比较完整的话来描述 InnoDB 的 MVCC,可以这样表述:

InnoDB 的 MVCC 依赖记录隐藏字段 trx_idroll_pointer、历史版本 undo log 以及可见性视图 Read View。事务执行快照读时,先检查当前版本的 trx_id 是否对当前 Read View 可见;若不可见,则通过 roll_pointer 沿着 undo log 回溯旧版本,直到找到一个可见版本。Read CommittedRepeatable Read 的区别主要体现在 Read View 的生成和复用时机上。


21. 适合记忆的极简版

最后给一个适合背诵的版本:

  • trx_id:谁改的
  • undo log:旧版本在哪
  • Read View:我能看谁
  • MVCC:写新版本,读可见版本
comments如果有不同意见或者补充,直接留在这里。
contact

在别处继续找到我

如果你想聊技术、设计,或者只是打个招呼。

暂未配置外部链接