关于MVCC
MySQL InnoDB MVCC 笔记
1. 什么是 MVCC
MVCC 是 Multi-Version Concurrency Control,中文通常叫 多版本并发控制。
它不是一种隔离级别,而是数据库为了实现事务隔离和提高并发性能所采用的一种机制。
MVCC 的核心思想很简单:
- 一条记录在逻辑上可以存在多个版本
- 读事务不一定读取最新版本
- 而是读取对自己可见的那个版本
可以先记一句最重要的话:
MVCC 的本质是:写生成新版本,读按可见性规则读取旧版本或当前版本。
2. 为什么需要 MVCC
如果数据库只有锁机制,没有 MVCC,那么并发场景下会出现很多阻塞:
- 事务 A 正在读,事务 B 想写,可能需要等待
- 事务 B 正在写,事务 A 想读,也可能需要等待
这样会导致吞吐下降,尤其是在读多写少的业务里表现更明显。
MVCC 的目标就是:
- 尽量让普通读不加锁
- 让读写尽量并发执行
- 在保证隔离性的同时提高系统性能
所以 MVCC 的价值主要有两点:
- 提升并发性能
- 支持一致性读
3. MVCC 解决了什么问题
MVCC 主要用来解决:
- 脏读
- 不可重复读的一部分问题
- 读写冲突过多的问题
但要注意,MVCC 不是万能的,它并不能独立解决所有并发问题,比如:
- 写写冲突仍然需要锁来控制
- 幻读是否完全避免,取决于数据库实现和具体读类型
Serializable级别通常不能只靠 MVCC
4. InnoDB 里 MVCC 的三个核心
如果只看 MySQL InnoDB,MVCC 最核心的三个概念是:
trx_idundo logRead View
可以先用一句话概括它们的职责:
trx_id:这个版本是谁改的undo log:当前版本不可见时,去哪里找旧版本Read View:当前事务能看见哪些版本
5. trx_id:记录版本的修改者
在 InnoDB 中,一条聚簇索引记录里除了业务字段,还会带一些隐藏字段,其中和 MVCC 关系最紧密的是:
trx_idroll_pointer
其中:
trx_id表示最后一次修改这条记录的事务 IDroll_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 有两个主要作用:
- 事务回滚
- 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 的事务 IDm_ids:创建 Read View 时,系统中活跃事务 ID 的集合min_trx_id:活跃事务中的最小事务 IDmax_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 里。
如果 X 在 m_ids 中,说明:
- 当前快照创建时,这个事务还没有提交
结果:
- 不可见
如果 X 不在 m_ids 中,说明:
- 它虽然落在这个区间内,但在快照创建前已经提交
结果:
- 可见
10.4 X == creator_trx_id
如果这个版本是当前事务自己写出来的:
- 可见
否则事务自己改完的数据自己都看不到,显然不合理。
11. 三者如何协同工作
trx_id、undo log、Read View 配合起来,才构成完整的 MVCC。
一次快照读的大致流程如下:
- 先拿到记录的当前版本
- 查看当前版本的
trx_id - 根据当前事务的
Read View判断这个版本是否可见 - 如果可见,直接返回
- 如果不可见,就通过
roll_pointer去找对应的undo log - 回溯到更老的版本
- 重复判断,直到找到第一个可见版本
一句话总结这个流程:
当前版本先看
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 读这条记录时会这样判断:
- 先看当前版本
300,其trx_id=20 - 发现
T20还活跃,因此不可见 - 顺着
undo log找到旧版本200 - 发现
T10已提交,因此可见 - 返回
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 CommittedRepeatable Read
14.1 Read Committed
特点:
- 每次快照读都会生成新的
Read View
这意味着:
- 同一个事务里两次
SELECT - 可能看到不同结果
所以它避免了脏读,但不能避免不可重复读。
14.2 Repeatable Read
特点:
- 通常第一次快照读生成
Read View - 整个事务期间复用这个
Read View
这意味着:
- 同一个事务里多次读同一行
- 结果保持一致
所以它可以避免不可重复读。
14.3 核心区别
Read Committed 和 Repeatable 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_id和roll_pointer、历史版本undo log以及可见性视图Read View。事务执行快照读时,先检查当前版本的trx_id是否对当前Read View可见;若不可见,则通过roll_pointer沿着undo log回溯旧版本,直到找到一个可见版本。Read Committed和Repeatable Read的区别主要体现在Read View的生成和复用时机上。
21. 适合记忆的极简版
最后给一个适合背诵的版本:
trx_id:谁改的undo log:旧版本在哪Read View:我能看谁- MVCC:写新版本,读可见版本