一次彻底搞懂 MySQL MVCC:从“快照”到“可见性”的完整思维链路
在我真正理解 MySQL MVCC 之前,总觉得“多版本控制”像个虚的概念。直到自己顺着“普通 SELECT 也有事务”这条线往下挖,才发现 MVCC 的世界比想象的更精巧。下面是我理清思路后的完整笔记,帮你在脑中构建一个能动起来的 MVCC 模型。
一、从一个问题开始:普通 SELECT 也有事务?
很多人以为只有显式的 BEGIN...COMMIT 才是事务,其实 任何 SQL 语句都会在事务上下文中执行。
即使是一条最简单的 SELECT,InnoDB 也会隐式地执行:
开启事务 → 创建快照(Read View) → 查询 → 自动提交
也就是说,每一次查询其实都发生在“某个时间点的世界里”。
这个快照不是复制一份数据,而是一份逻辑视图,用来决定哪些版本对当前事务可见。
要点:****
- SELECT 也在事务中执行,只是自动开启自动提交。
- “快照”是逻辑概念,不是整表副本。
- 一切可见性判断都依赖于事务。
二、MVCC 的核心:多版本行可见性
MVCC 全称 Multi-Version Concurrency Control,直译是“多版本并发控制”。
它解决的核心问题是:在高并发读写下,如何既不加锁又能读到一致的数据。****
当上千个事务同时修改同一张表时,每条记录其实不止一个版本。
InnoDB 通过三类隐藏字段来维护这些版本信息:
| 字段 | 含义 |
|---|---|
| DB_TRX_ID | 最近修改该行的事务 ID |
| DB_ROLL_PTR | 指向旧版本(Undo 日志)的指针 |
| DB_ROW_ID | 当表没有主键时的系统行号 |
当你执行查询时,InnoDB 并不会直接返回“当前页上的最新值”,而是通过“可见性判断”来决定该返回哪个版本。
三、可见性判断:数据库的时间旅行器
查询一行数据时,InnoDB 会将当前行的 DB_TRX_ID 与事务快照(Read View)进行比较。
快照里保存了:
-
当前活跃事务列表;
-
最大已分配事务 ID(上界);
-
当前事务 ID。
判断逻辑可以简化为:
flowchart TD
A[读取行版本] --> B{该版本在快照前已提交?}
B -- 是 --> C[可见 返回该版本]
B -- 否 --> D{是否存在 Undo 旧版本?}
D -- 是 --> E[沿 Roll Pointer 取旧版本 重试]
D -- 否 --> F[不可见 此行在快照中不存在]
这套机制就像“时间旅行”:
你拿着一张“快照通行证”,在时间博物馆里浏览展品。
每件展品都有时间戳和修改记录,系统会根据你的通行证判断:
“这件东西在你进入之前就存在吗?如果不是,请看它的旧版本。”
要点:****
- 快照决定“你能看到哪些事务的结果”;
- 行的隐藏字段决定“这个版本属于谁”;
- 可见性判断将两者结合,实现真正的多版本控制。
四、Undo 日志与版本链:回到过去的通道
每次 UPDATE 或 DELETE,旧版本都会被写入 Undo 日志。
DB_ROLL_PTR 就像一条回溯指针,串起了整条“版本链”。
查询时,如果当前版本对事务不可见,就沿着这条链回退,直到找到一个符合快照条件的版本。
而当没有任何事务再需要旧版本时,后台 Purge 线程 会清理这些历史数据。
要点:****
- Undo 是历史版本仓库。
- Roll Pointer 串起版本链。
- Purge 清理不再需要的历史版本。
五、隔离级别下的快照时机
不同隔离级别下,Read View 的创建时机不同:
| 隔离级别 | 快照创建时机 | 读到的效果 |
|---|---|---|
| REPEATABLE READ | 第一次一致性读时创建,事务内复用 | 同一事务多次 SELECT 结果一致 |
| READ COMMITTED | 每次一致性读都新建快照 | 能看到其他事务新提交的数据 |
提醒:****
- 两者都不会产生脏读。
- 区别仅在于快照的“生命周期”。
六、遗漏与轻度补全
你的笔记几乎涵盖了 MVCC 的关键流程,这里补一个常被忽略的点:
“锁定读”不使用快照。
像 SELECT ... FOR UPDATE、LOCK IN SHARE MODE 这类查询,会直接加锁读取最新数据,不走一致性读路径。
七、总结复盘:四个记忆钉
-
每个查询都有事务,即使是自动提交模式。
-
快照是事务级视图,不是物理副本。
-
行的隐藏字段 + Undo 构成版本链,Read View 决定可见性。
-
高并发靠“版本可见性”维持一致性,而非靠加锁。
学会从“时间旅行”的角度看待 InnoDB,每一条 SELECT 都是一次穿越:
去看一个时间点上,对你而言“存在”的世界。