前言
你想深入了解 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(快照读、无锁),写操作 / 锁定读走锁(当前读、加锁),二者结合实现了高效且一致的并发控制。
到此这篇关于mysql innodb中mvcc与锁的具体协作流程的文章就介绍到这了,更多相关mysql innodb中mvcc与锁协作内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论