小白也能懂的MVCC详解
MVCC 的全称是 Multi-Version Concurrency Control,即多版本并发控制。它是数据库中用于解决并发读写冲突的核心技术,目的是让 读写操作互不阻塞,提升数据库的并发性能,同时还能保证事务的隔离性。
一、为什么需要MVCC?先看并发下的“坑”
如果没有MVCC,数据库只能靠锁来处理并发,会出现很多问题:
- 读锁阻塞写:一个事务读数据时,会加读锁,其他事务想修改这条数据就会被阻塞。
- 写锁阻塞读:一个事务修改数据时,会加写锁,其他事务想读这条数据也会被阻塞。
- 隔离性问题:并发操作时,容易出现脏读、不可重复读、幻读(这三个是事务隔离性的三大问题)。
而MVCC的出现,就是为了在不加锁的情况下,让读写操作并行执行,同时避免上述隔离性问题。
核心思路很简单:给每行数据保存多个历史版本,不同事务看到的数据版本不同。
二、MVCC的核心概念:3个隐藏字段 + 2个关键日志
要实现多版本,数据库会在每行数据的背后偷偷做手脚,同时依赖两类日志,我们以 MySQL InnoDB 引擎为例(主流数据库的MVCC实现逻辑类似)。
1. 每行数据的3个隐藏字段
InnoDB 会为每一行记录自动添加3个隐藏字段,它们是实现MVCC的基础:
| 隐藏字段 | 作用 |
|---|---|
DB_TRX_ID |
事务ID:记录最后一次修改(插入/更新)这条数据的事务ID |
DB_ROLL_PTR |
回滚指针:指向这条数据的上一个历史版本(存放在 undo log 中) |
DB_ROW_ID |
行ID:如果表没有主键,InnoDB会自动生成这个字段作为聚簇索引,和MVCC关联不大 |
2. 两个关键日志
(1)undo log:数据的“历史版本仓库”
undo log 是回滚日志,它的核心作用是:
- 当事务执行更新/删除操作时,会先把数据的旧版本保存到
undo log中。 - 事务回滚时,可以用
undo log恢复数据。 - 给MVCC提供历史版本:通过
DB_ROLL_PTR指针,能串联起一行数据的所有历史版本,形成一个版本链。
举个例子:一行数据被3个事务依次修改,它的版本链长这样:
最新版本(当前数据页) ← DB_ROLL_PTR ← 版本2(undo log) ← DB_ROLL_PTR ← 版本1(undo log)
(2)redo log:保证数据不丢(辅助作用)
redo log 是重做日志,主要用于保证数据库崩溃后的数据恢复,和MVCC的核心逻辑无关,这里只需要知道它是InnoDB的“保障”即可。
三、MVCC的核心:Read View(一致性视图)
当事务执行读操作(SELECT)时,InnoDB 会为这个事务生成一个 Read View(一致性视图),这个视图是判断数据版本是否可见的“规则手册”。
1. Read View 包含的4个关键参数
| 参数 | 含义 |
|---|---|
m_ids |
当前活跃的事务ID列表(即还没提交的事务ID) |
min_trx_id |
m_ids 中的最小事务ID |
max_trx_id |
系统下一个要分配的事务ID(大于当前所有已分配的事务ID) |
creator_trx_id |
生成这个 Read View 的事务ID |
2. 版本可见性判断规则
拿到 Read View 后,事务会去对比当前数据版本的 DB_TRX_ID(最后修改这个版本的事务ID)和 Read View 的参数,判断这个版本是否可见。规则如下(重点):
- 如果
DB_TRX_ID == creator_trx_id:这个版本是当前事务自己修改的,可见。 - 如果
DB_TRX_ID < min_trx_id:修改这个版本的事务已经提交了,可见。 - 如果
DB_TRX_ID >= max_trx_id:修改这个版本的事务是在 Read View 生成后才开启的,不可见。 - 如果
min_trx_id <= DB_TRX_ID < max_trx_id:- 若
DB_TRX_ID在m_ids中:事务还没提交,不可见。 - 若
DB_TRX_ID不在m_ids中:事务已经提交,可见。
- 若
如果当前版本不可见,就通过 DB_ROLL_PTR 去 undo log 找上一个历史版本,重复上述判断,直到找到可见的版本,或者版本链结束(返回空)。
四、举个例子:彻底看懂MVCC的执行过程
假设数据库里有一张 user 表,初始数据如下:
| id | name | DB_TRX_ID | DB_ROLL_PTR |
|---|---|---|---|
| 1 | 张三 | 100 | null |
现在有3个事务并发执行,事务ID分别是 200、201、202,执行顺序如下:
| 时间 | 事务200(未提交) | 事务201(读) | 事务202(已提交) |
|---|---|---|---|
| T1 | 执行 UPDATE user SET name='李四' WHERE id=1 |
- | - |
| T2 | - | 执行 SELECT * FROM user WHERE id=1 |
- |
| T3 | - | - | 执行 UPDATE user SET name='王五' WHERE id=1,提交事务 |
| T4 | - | 再次执行 SELECT * FROM user WHERE id=1 |
- |
步骤1:T1 事务200修改数据
事务200更新数据时,InnoDB 会:
- 把原数据(name=张三,DB_TRX_ID=100)保存到
undo log。 - 更新当前数据页:name=李四,
DB_TRX_ID=200,DB_ROLL_PTR指向 undo log 里的旧版本。 - 此时事务200未提交,属于活跃事务。
步骤2:T2 事务201第一次查询
事务201执行查询,生成 Read View:
m_ids = [200](活跃事务只有200)min_trx_id = 200max_trx_id = 203(下一个要分配的事务ID)creator_trx_id = 201
然后判断当前数据版本(DB_TRX_ID=200):
- 200 在
m_ids中 → 不可见。 - 通过
DB_ROLL_PTR找到 undo log 里的旧版本(DB_TRX_ID=100)。 - 100 < min_trx_id(200) → 可见。
- 所以事务201查到的 name 是 张三。
步骤3:T3 事务202修改并提交
事务202更新数据时:
- 把当前数据(name=李四,DB_TRX_ID=200)保存到
undo log。 - 更新数据页:name=王五,
DB_TRX_ID=202,DB_ROLL_PTR指向 undo log 里的李四版本。 - 事务202提交,从活跃事务列表中移除。
步骤4:T4 事务201第二次查询
事务201是可重复读隔离级别,整个事务只会生成一次 Read View(关键!),所以还是用 T2 生成的 Read View(m_ids=[200])。
判断当前数据版本(DB_TRX_ID=202):
- 202 >= max_trx_id(203)?不,202 < 203。
- 202 在 m_ids([200])里?不。
- 但 202 > min_trx_id(200),且 Read View 生成时,事务202还没开启(max_trx_id是203,202是有效的,但不在活跃列表)→ 按照规则,是否可见?
→ 注意:可重复读隔离级别下,Read View 是事务启动时生成的,此时事务202还没修改数据,所以事务201看不到事务202的修改结果。 - 继续找历史版本,直到找到 DB_TRX_ID=100 的版本 → 可见。
- 所以第二次查询结果还是 张三 → 这就是可重复读的实现!
五、MVCC和事务隔离级别的关系
MVCC 主要在 MySQL 的两个隔离级别下工作:
-
读已提交(Read Committed)
- 每次执行
SELECT都会重新生成 Read View。 - 能看到已提交的事务修改结果 → 解决脏读,但可能出现不可重复读。
- 每次执行
-
可重复读(Repeatable Read)
- 整个事务只在第一次 SELECT 时生成一次 Read View。
- 不管其他事务怎么修改,只要没提交,当前事务看到的都是同一个版本 → 解决脏读、不可重复读,InnoDB 还通过间隙锁解决了幻读。
提示:读未提交(Read Uncommitted)直接读最新数据,串行化(Serializable)直接用锁,都不依赖 MVCC。
六、MVCC的优点和缺点
优点
- 读写不阻塞:读操作不用加锁,写操作也不会阻塞读操作 → 极大提升并发性能。
- 保证隔离性:在可重复读/读已提交级别下,避免脏读、不可重复读等问题。
- 数据一致性:每个事务看到的都是符合自己视图的一致数据。
缺点
- 存储开销:需要保存数据的多个历史版本,undo log 会占用额外的磁盘空间。
- 清理开销:数据库需要定期清理过期的 undo log(比如事务都提交了,旧版本没用了),这会带来一定的性能消耗。
七、总结
MVCC 的本质就是 “版本链 + Read View”:
- 用版本链保存数据的历史修改记录。
- 用Read View 判断当前事务能看到哪个版本的数据。
- 最终实现 读写并行,同时保证事务隔离性。
它是数据库并发控制的“神器”,也是理解 InnoDB 事务机制的关键。
我可以帮你整理一份MVCC与锁机制的对比清单,方便你更清晰地理解两者的适用场景,需要吗?

浙公网安备 33010602011771号