你想深入了解 MySQL InnoDB 中 MVCC 与锁的具体协作流程,这是理解两者配合的关键。接下来我会从 InnoDB 实现 MVCC 的核心组件入手,一步步拆解它的工作原理,以及和锁如何分工协作,全程用通俗的语言 + 实例,让你彻底搞懂。
一、MVCC 的核心实现基础(3 个关键组件)
InnoDB 不是凭空实现 MVCC 的,而是依赖 3 个核心机制,这是理解后续流程的前提:
1. 行记录的隐藏字段
InnoDB 会为每一行数据(除了用户定义的字段)自动添加 3 个隐藏字段,用来标记版本信息:
| 隐藏字段 | 作用 |
|---|---|
DB_TRX_ID |
最后一次修改(插入 / 更新)该行数据的事务 ID(递增的唯一编号) |
DB_ROLL_PTR |
回滚指针,指向该行数据的历史版本(存储在 undo log 中) |
DB_ROW_ID |
行唯一 ID(如果表没有主键 / 唯一索引,InnoDB 会自动生成) |
可以把这 3 个字段理解为:给每行数据贴了一个 "版本标签",记录了 "谁最后改的""历史版本在哪"。
2. Undo Log(回滚日志)
Undo Log 是 MVCC 的 "版本仓库",作用有两个:
- 存储数据的历史版本:每次修改数据(UPDATE/DELETE),InnoDB 会先把修改前的数据拷贝到 Undo Log 中,形成一个版本链;
- 事务回滚:如果事务执行失败,可通过 Undo Log 恢复数据到修改前的状态。
举个例子:
- 事务 100 插入一行数据:
DB_TRX_ID=100,DB_ROLL_PTR=null(无历史版本); - 事务 200 更新这行数据:InnoDB 先把 "事务 100 版本" 拷贝到 Undo Log,再修改当前行的
DB_TRX_ID=200,DB_ROLL_PTR指向 Undo Log 中的 "100 版本"; - 事务 300 再更新这行数据:同理,Undo Log 中会新增 "200 版本",当前行
DB_TRX_ID=300,DB_ROLL_PTR指向 "200 版本"; - 最终形成版本链:当前行(300)→ Undo Log 中的 200 版本 → Undo Log 中的 100 版本。
3. Read View(读视图)
Read View 是 MVCC 的 "版本筛选规则",每个事务在执行快照读(普通 SELECT)时,会生成一个 Read View,用来判断当前事务能 "看到" 哪个版本的数据。
Read View 包含 4 个核心参数:
m_ids:当前活跃的事务 ID 列表(即还没提交的事务);min_trx_id:m_ids中的最小值;max_trx_id:系统下一个要分配的事务 ID(大于当前所有已分配的);creator_trx_id:生成这个 Read View 的事务 ID。
核心判断规则(判断版本链中的某个版本是否可见):
- 如果版本的
DB_TRX_ID < min_trx_id:该版本是 "已提交事务" 修改的,可见; - 如果版本的
DB_TRX_ID >= max_trx_id:该版本是 "未来事务" 修改的,不可见; - 如果
min_trx_id ≤ DB_TRX_ID < max_trx_id:- 若
DB_TRX_ID在m_ids中(事务还没提交):不可见; - 若
DB_TRX_ID不在m_ids中(事务已提交):可见;
- 若
- 特殊:如果版本的
DB_TRX_ID = creator_trx_id(自己修改的):可见。
二、MVCC + 锁 协作的完整实例(可重复读隔离级别)
为了让你更直观理解,我们用一个具体场景拆解(假设初始数据:id=1,name="张三",DB_TRX_ID=0,DB_ROLL_PTR=null):
场景:两个并发事务
| 时间 | 事务 A(trx_id=101) | 事务 B(trx_id=102) |
|---|---|---|
| T1 | 开始事务 | - |
| T2 | SELECT * FROM t WHERE id=1;(快照读) | - |
| T3 | - | 开始事务 |
| T4 | - | UPDATE t SET name="李四" WHERE id=1;(写操作,加行锁) |
| T5 | SELECT * FROM t WHERE id=1;(快照读) | - |
| T6 | - | 提交事务 |
| T7 | SELECT * FROM t WHERE id=1;(快照读) | - |
| T8 | SELECT * FROM t WHERE id=1 FOR UPDATE;(当前读) | - |
| T9 | 提交事务 | - |
分步拆解:
1. T2:事务 A 第一次快照读
- 生成 Read View:
m_ids=[101](只有自己活跃),min_trx_id=101,max_trx_id=103,creator_trx_id=101;
- 读取 id=1 的行:当前行
DB_TRX_ID=0 < min_trx_id=101,可见; - 结果:name="张三"(无锁,不阻塞任何操作)。
2. T4:事务 B 执行 UPDATE(写操作)
- 锁的作用 :InnoDB 对 id=1 的行加行级排他锁(X 锁),防止其他事务同时修改这行;
- MVCC 的作用 :
- 把当前行(
DB_TRX_ID=0,name="张三")拷贝到 Undo Log,形成版本 0; - 修改当前行:name="李四",
DB_TRX_ID=102,DB_ROLL_PTR指向 Undo Log 中的版本 0;
- 把当前行(
- 此时版本链:当前行(102)→ 版本 0(0)。
3. T5:事务 A 第二次快照读
- 可重复读隔离级别下,事务 A 复用 T2 生成的 Read View(
m_ids=[101],min_trx_id=101); - 检查当前行的
DB_TRX_ID=102:101 ≤ 102 < 103,且 102 不在m_ids=[101]中?但因为是复用 Read View,InnoDB 会沿着版本链找可见版本;- 找到 Undo Log 中的版本 0(
DB_TRX_ID=0 < 101),可见;
- 结果:还是 name="张三"(读不阻塞写,且保证可重复读)。
4. T6:事务 B 提交
- 释放 id=1 行的排他锁(锁的使命完成);
- Undo Log 不会立即删除(后续由 purge 线程清理),仍为 MVCC 提供版本。
5. T7:事务 A 第三次快照读
- 仍复用 T2 的 Read View,因此还是读取版本 0;
- 结果:依然是 name="张三"(可重复读的核心体现)。
6. T8:事务 A 执行当前读(SELECT ... FOR UPDATE)
- 锁的作用:此时不走 MVCC,直接加行级排他锁,读取最新版本;
- MVCC 不生效 :跳过版本链,直接读取当前行(
DB_TRX_ID=102,name="李四"); - 结果:name="李四"(当前读必须拿到最新数据,且加锁防止其他写操作)。
三、锁和 MVCC 的分工总结(InnoDB 视角)
| 环节 | 锁的角色 | MVCC 的角色 |
|---|---|---|
| 快照读(普通 SELECT) | 无锁(完全不参与) | 生成 Read View,遍历版本链找可见版本 |
| 写操作(UPDATE/DELETE/INSERT) | 加行级排他锁,防止写冲突 | 生成数据新版本,维护版本链 |
| 当前读(SELECT ... FOR UPDATE) | 加行级锁,阻塞其他写操作 | 不生效,直接读取最新版本 |
| 事务隔离级别 | 仅靠锁只能实现 "读未提交 / 串行化" | 实现 "读已提交 / 可重复读" 核心逻辑 |
总结
- MVCC 的核心实现三要素:行隐藏字段(版本标记)、Undo Log(版本仓库)、Read View(版本筛选规则),三者共同实现了 "快照读";
- 锁与 MVCC 的协作逻辑:锁负责解决 "写冲突"(排他锁),MVCC 负责解决 "读写阻塞" 和 "读一致性"(快照读);
- 关键区分:普通 SELECT 走 MVCC(快照读、无锁),写操作 / 锁定读走锁(当前读、加锁),二者结合实现了高效且一致的并发控制。