1. Overview
前面介绍的各种并发控制协议(如两阶段封锁、乐观并发)都会因为不同事务之间的读写、写读、写写冲突而造成阻塞,影响事务的并发性能。多版本并发控制协议执行写操作时会创建一个新版本,数据库中存在多个不同版本的历史数据,事务执行读操作时只会根据其时间戳读取一个应该看到的历史版本。MVCC由于读写不会发生冲突,因此并发性能效果会更好,缺点在于众多的历史版本会占据较多的存储空间。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%201.png)
存储历史版本并非一无用处,MVCC使用历史版本可以支持历史数据查询。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%202.png)
下面是一个MVCC的例子,假设当前设置的事务并发隔离级别是串行化。首先,T1和T2都被分配了时间戳,分别是1和2。状态A的初始版本为A0,值为123,T1检查A0的时间戳小于它的时间戳且为最新值,读取A0的值。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%203.png)
T2写入一个A的新值A1,且Begin的时间戳为2,将A0的End处时间戳标为2。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%204.png)
T1读取A,由于前面提到隔离级别是串行化且T1的时间戳为1,因此这时候T1读取的依旧是A0。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%205.png)
下面是另一个MVCC并发控制例子,首先T1写A,创建新版本A1,将A0的End标记为1。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%206.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%207.png)
T2读取A,由于隔离级别设置为串行化且T1还没有被提交,因此T2读到的A状态依旧为A0。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%208.png)
随后T2准备写A,由于隔离级别设置为串行化但是A现在已经被T1写入且End还未标记,所以阻塞(感觉这边可以不阻塞,后面读的时候根据Begin和End以及自己的时间戳判断该读哪个就行了)。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%209.png)
T1继续读A,这时应该读A1,因为T1在前面写了A1。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%209.png)
T1在COMMIT了之后,T2可以继续执行。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2010.png)
2. MVCC Control Design
MVCC 不同于之前提到的并发控制协议,它本质上通过多版本实现了基于快照隔离(Snapshot Isolation,SI)的事务并发机制。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2013.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2014.png)
多版本数据的存储也是MVCC中需要解决的问题,同时能够方便事务检查出当前对其可见的历史状态。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2015.png)
主要存储方式有三种(非常常见的存储策略)。其中,Append-Only每次将新版本数据直接追加到相同的存储表中;Time-Travel将老版本单独存储到一张表中;Delta Storge只将本次事务修改的值存放到单独的一张表中。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2016.png)
Append-Only 每次将新版本数据追加到Main Table中,Main Table保存所有的历史状态数据。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2017.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2018.png)
而 version chain 中值的排列顺序可以按照从旧到新或者从新到旧的顺序排列。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2019.png)
Time Travel 每次将旧的版本放在Time Travel表中,Main Table存放新的值。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2020.png)
Delta Storage 每次仅将变化的部分存入单独的 Delta Storage 表中。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2022.png)
对于版本过旧,不会再有交易访问的数据,DBMS将会对其删除释放空间。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2023.png)
垃圾回收有两种粒度,一个是Tuple级,一个是Transaction级。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2024.png)
DBMS单独启动一个线程扫描数据库,决定各个版本是否需要清除。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2025.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2026.png)
或者不需要DBMS单独启动一个线程负责清理数据,把这个事情放在每次数据库操作中,当访问多版本数据时顺带将数据更新(一种协同操作的概念,本质还是一种pipline)。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2027.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2028.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2029.png)
事务级别垃圾回收,每当一个事务结束时候DBMS判断其是否对于其他事务可见。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2030.png)
不同事务为了快速找到version chain,利用索引来加速,索引可以直接存物理地址或者设置一个中间层,这样可以减少多个索引更新的开销。
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2031.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2032.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2033.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2034.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2035.png)
![](https://github.com/HiBo001/CMU445_pic/raw/main/19/图片%2036.png)