MVCC的实现原理

MVCC的提出
MVCC,全称Multi-Version Concurrency Control,多版本并发控制。
- 高并发环境下,如果我们想保证并发安全, 那么最简单的就是加锁,线程排队一个一个去执行自己的事务。但这样会导致性能大幅降低,无法满足高并发对性能的需求。
- 于是,我们想要对其进行优化。深入去看数据库的执行过程,我们发现读取操作会加锁导致写入操作被阻塞,必须等待读取完成后才能写入。这就是读写冲突。
- 为了解决这种读写冲突,我们便想,能不能在读的时候不加锁呢,让读操作去读一个
过去的一张照片(快照),而写操作去创建未来的一个新版本。这样大家各走各的路,互不干扰。于是,我们便提出了MVCC,多版本并发控制。
MVCC如何实现
1. 隐藏的字段
- 在InnoDB中,每一行数据有三个隐藏的字段
DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID。其中最重要的便是前两个字段。 DB_TRX_ID,表示最近一次修改它的事务ID。DB_ROLL_PTR,表示指向undo log的指针。
2. Undo Log(版本链)
- 当你对数据库进行操作时,
Undo Log会记录一条相反的操作。比如你INSERT,它就会记录一条DELETE;你把 10 更新到 20,它就会记录更新到10;你DELETE,它就会记录INSERT。 - 当一条数据被修改的时候,旧数据不会被直接删除,而是会被写进
Undo Log,并由DB_ROLL_PTR串联起来。 - 这就形成了版本链:
1 | 版本5 |
3. Read View(读视图)
一张照片,便此刻定格了下来。
- 当一个事务开启时:生成一个
Read View。 Read View里面记录着:
1 | m_ids |(活跃中的事务id列表) |
4. 可见性算法
- 查询数据时,数据库会判断:这个版本的
trx_id对当前事务是否“可见”? - 下面是判断规则:

5. 隔离等级
- 在MVCC中有两种隔离级别:
可重复读,读已提交。 - 其中我们需要注意的是
可重复读再第二次查询的时候不会再次生成Read View,而是继续沿用第一次的视图,保证数据的一致性。
6. 实际应用
- 接下来我们以此为例进行综合应用
![image]()
- 以事务104来看。此时正在执行的事务是事务104和事务101
m_ids = {101,104}min_trx_id = 101max_trx_id = 105creator_trx_id = 104![image]()
- 接下来依据可见性算法进行判断。
- 103 ≠ 104 不是当前事务
- 103 ≮ 101 不是已提交的事务
- 103 ≯ 105 不是当前事务之后开启的事务
- 101 ≤ 103 ≤ 105,且不在
m_ids中,所以事务103是已提交的事务,可以访问103的值。 - 第一个查询结果为axing3。
执行第二个查询时,会根据上文提到的隔离级别出现结果的不同。
- 在执行第二个查询之前,按照流程图,事务101已经提交了事务。Undo Log多出了事务101的节点。
![image]()
- 在读提交的隔离级别下,101 < 104,说明101事务已提交,所以第二次的查询结果是axing1。
- 此时第一次查询和第二次查询结果确实不一样,出现了不可重复读。
- 为了实现可重复读,在第二次查询时不会生成新的ReadView,而是沿用第一次查询的ReadView。此时的组合是Undo Log(新) + ReadView(旧)
![image]()
- 此时101在m_ids中,属于未提交事务,是不可读取的,于是我们便向下找到103节点。
此时情况便和最开始的第一次查询情况一样了。 ![image]()
- 此时两次查询结果都是相同的了,实现了可重复读。
MVCC中的思想
用空间换取时间的自由,通过保留历史来成就当前的并发。
在传统的锁定机制中,世界是排他的:我要写,你就不能读。这是一种对抗性思维。
MVCC: 读和写不一定是冲突的。我通过为数据创建多个版本,读操作可以查看“过去某个时间点”的快照,而写操作则在“当前时间点”创建新版本,我让不同的事务生活在不同的“时间流”里,互不干扰。
让“过去”与“现在”解耦
数据不再是一个静止的、唯一的点,而是一个随时间流动的序列。
MVCC:“真相”取决于你观察的角度(事务开始的时间点)。只要你的逻辑是一致的,你所看到的那个“旧版本”就是属于你的正确真相。
上善若水,水善利万物而不争
承认时间的相对性,让每一个事务都能在不干扰他人的情况下,拥有一个逻辑上完美的、属于自己的世界。
图片资源来源B站:阿星不是程序员




