# Multi-Version Concurrency Control

## 简介

简而言之，实现 MVCC 的 DBMS 在内部维持着单个逻辑数据的多个物理版本，当事务修改某数据时，DBMS 将为其创建一个新的版本；当事务读取某数据时，它将读到该数据在事务开始时刻之前的最新版本。

MVCC 首次被提出是在 1978 年的一篇  MIT 的博士[论文](https://web.archive.org/web/20051025124412/http://www.lcs.mit.edu/publications/specpub.php?id=773)中。在 80 年代早期，DEC 的 Rdb/VMS 和 InterBase 首次真正实现了 MVCC，其作者是 Jim Starkey，NuoDB 的联合创始人。如今，Rdb/VMS 成了 Oracle Rdb，InterBase 成为开源项目 Firebird。

## MVCC

MVCC 的核心优势可以总结为以下两句话：

> Writers don't block readers.\
> 写不阻塞读
>
> Readers don't block writers.\
> 读不阻塞写

只读事务无需加锁就可以读取数据库某一时刻的快照，如果保留数据的所有历史版本，DBMS 甚至能够支持读取任意历史版本的数据，即 time-travel。

### Example #1

事务$$T\_1$$和 $$T\_2$$分别获得时间戳 1 和 2，二者的执行过程如下图所示。开始前，数据库存有数据 A 的原始版本 $$A\_0$$， $$T\_1$$先读取 A 数据：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6w3znotWi_QUedNdUM%2FScreen%20Shot%202020-05-10%20at%208.54.51%20AM.jpg?alt=media\&token=c42113db-6817-4610-b248-ddfacfe0fc79)

然后 $$T\_2$$修改 A 数据，这时 DBMS 中将增加 A 数据的新版本 $$A\_1$$，同时标记 $$A\_1$$的开始时间戳为 2， $$A\_0$$ 的结束时间戳为 2：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6w5WIFs176i5aGE8xg%2FScreen%20Shot%202020-05-10%20at%209.01.32%20AM.jpg?alt=media\&token=cf64558a-dadf-448d-a01d-e7e6d6060548)

$$T\_1$$再次读取 A，因为它的时间戳为 1，根据记录的信息，DBMS 将 $$A\_0$$返回给 $$T\_1$$ ：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6w64OXrZix0VIfum6z%2FScreen%20Shot%202020-05-10%20at%209.03.57%20AM.jpg?alt=media\&token=b2925d4b-0a7e-4080-b6d3-f8344f5d0378)

### Example #2

例 2 与例 1 类似，$$T\_1$$先修改数据 A：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6w80iLmnLvJtnOBp3f%2FScreen%20Shot%202020-05-10%20at%209.12.21%20AM.jpg?alt=media\&token=5201b2b7-d2e3-492a-a9c6-258cf1381e78)

此时 $$T\_2$$读取 A，由于 $$T\_1$$尚未提交， $$T\_2$$ 只能读取 $$A\_0$$：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6w8QTLPWxLjK7LnMyv%2FScreen%20Shot%202020-05-10%20at%209.14.14%20AM.jpg?alt=media\&token=9b7506f5-3fdf-43cc-962d-7106b3d99adf)

$$T\_2$$想修改 A，但由于有另一个活跃的事务 $$T\_1$$正在修改 A ， $$T\_2$$ 需要等待 $$T\_1$$提交后才能继续推进：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6w9Qrm0m_6fjfgbK0C%2FScreen%20Shot%202020-05-10%20at%209.18.37%20AM.jpg?alt=media\&token=a15c7bd3-141e-4684-9184-a2050d910622)

$$T\_1$$提交后， $$T\_2$$创建了 A 的下一个版本 $$A\_2$$ ：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wAQUT4F7k_-B-UPHF%2FScreen%20Shot%202020-05-10%20at%209.22.58%20AM.jpg?alt=media\&token=f1027429-6cb8-4cdb-ae43-a9f263079fd9)

### 小结

MVCC 并不只是一个并发控制协议，并发控制协议只是它的一个组成部分。它深刻地影响了 DBMS 管理事务和数据的方式，使用 MVCC 的 DBMS 数不胜数：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wCxNsNNNJhs4d3hhi%2FScreen%20Shot%202020-05-10%20at%209.34.02%20AM.jpg?alt=media\&token=01d21bd3-8bb0-4a17-ae03-b343e42f71d6)

## Design Decisions

上文提到，MVCC 不止是一个并发控制协议，它由许多部分组成，这些部分包括：

* Concurrency Control Protocol
* Version Storage
* Garbage Collection
* Index Management

每一部分都可以选择不同的方案，可以根据具体场景作出最优的设计选择。

### Concurrency Control Protocol

前面 2 节课已经介绍了各种并发控制协议，MVCC 可以选择其中任意一个：

**Approach #1：**&#x54;imestamp Ordering (T/O)：为每个事务赋予时间戳，并用以决定执行顺序\
**Approach #2：**&#x4F;ptimistic Concurrency Control (OCC)：为每个事务创建 private workspace，并将事务分为 read, write 和 validate 3 个阶段处理\
**Approach #3：**&#x54;wo-Phase Locking (2PL)：按照 2PL 的约定获取和释放锁

### Version Storage

如何存储一条数据的多个版本？DBMS 通常会在每条数据上拉一条版本链表 (version chain)，所有相关的索引都会指到这个链表的 head，DBMS 可以利用它找到一个事务应该访问到的版本。不同的版本存储方案在 version chain 上存储的数据不同，主要有 3 种存储方案：

**Approach #1：**&#x41;ppend-Only Storage：新版本通过追加的方式存储在同一张表中\
**Approach #2：**&#x54;ime-Travel Storage：老版本被复制到单独的一张表中\
**Approach #3：**&#x44;elta Storage：老版本数据的被修改的字段值被复制到一张单独的增量表 (delta record space) 中

#### Append-Only Storage

如下图所示，同一个逻辑数据的所有物理版本都被存储在同一张表上，每次更新时，就往表上追加一个新的版本记录，并在旧版本的数据上增加一个指针指向新版本：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wIKBUuYJaPcdP9h4x%2FScreen%20Shot%202020-05-10%20at%209.57.30%20AM.jpg?alt=media\&token=cc0796aa-6820-466d-8917-4634e4404e76)

再次更新的行为类似：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wIpJAGMj6lYI5gOSD%2FScreen%20Shot%202020-05-10%20at%209.59.39%20AM.jpg?alt=media\&token=b7cd3f9e-34d4-41ef-bd7e-5b321fce02b9)

也许你已经注意到，指针的方向也可以从新到旧，二者的权衡如下：

Approach #1：Oldest-to-Newest (O2N)：写的时候追加即可，读的时候需要遍历链表\
Approach #2：Newest-to-Oldest (N2O)：写的时候需要更新所有索引指针，读的时候不需要遍历链表

#### Time-Travel Storage

单独拿一张表 (Time-Travel Table) 来存历史数据，每当更新数据时，就把当前版本复制到 TTT 中，并更新指针：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wPh9aHblLel0sS-sa%2FScreen%20Shot%202020-05-10%20at%2010.29.10%20AM.jpg?alt=media\&token=f29037f3-7ee0-4916-8f34-76e0b494d54d)

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wPoSQZMBPfhEDuk-2%2FScreen%20Shot%202020-05-10%20at%2010.30.13%20AM.jpg?alt=media\&token=31d80d5e-da3c-48d5-a894-02da424b881d)

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wQfNqA2Ck8XgSRQK2%2FScreen%20Shot%202020-05-10%20at%2010.33.58%20AM.jpg?alt=media\&token=a4d821cc-9ac9-431b-b47b-dee759e5f6fa)

#### Delta Storage

每次更新，仅将变化的字段信息存储到 delta storage segment 中：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wR-Ssq1MrdYZQKIeo%2FScreen%20Shot%202020-05-10%20at%2010.35.24%20AM.jpg?alt=media\&token=c99df96b-6869-42cf-bda5-09cc0ff04ad8)

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6w-z5vz1ijI0VRFJCo%2F-M6wR7n3rpKK_HYP7oUs%2FScreen%20Shot%202020-05-10%20at%2010.35.58%20AM.jpg?alt=media\&token=aa0100d4-d72f-4b09-94b0-9acdddb5d11b)

DBMS 可以通过 delta 数据逆向恢复数据到之前的版本。

### Garbage Collection

随着时间的推移，DBMS 中数据的旧版本可能不再会被用到，如：

* 已经没有活跃的事务需要看到该版本
* 该版本是被一个已经中止的事务创建

这时候 DBMS 需要删除这些可以回收的物理版本，这个过程也被称为 GC。在 GC 的过程中，还有两个附加设计决定：

* 如何查找过期的数据版本
* 如何确定某版本数据是否可以被安全回收

GC 可以从两个角度出发：

Approach #1：Tuple-level：直接检查每条数据的旧版本数据\
Approach #2：Transaction-level：每个事务负责跟踪数据的旧版本，DBMS 不需要亲自检查单条数据

#### Tuple-Level GC

*Background Vacuuming*

如下图所示，假设有 2 个活跃事务，它们的时间戳分别为 12 和 25：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6wT4DQo_NTjQlHPZKQ%2F-M6wV3UFiDiqRmgCxB1D%2FScreen%20Shot%202020-05-10%20at%2010.52.04%20AM.jpg?alt=media\&token=090e7eb0-d659-4295-98e5-eaf99edd75bb)

这时有个 Vacuum 守护线程会周期性地检查每条数据的不同版本，如果它的结束时间小于当前活跃事务的最小时间戳，则将其删除：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6wT4DQo_NTjQlHPZKQ%2F-M6wVQIGffqhvaKBhYnc%2FScreen%20Shot%202020-05-10%20at%2010.54.43%20AM.jpg?alt=media\&token=8c3cd0a5-dd66-4bba-b10e-7dceb88f6454)

为了加快 GC 的速度，DBMS 可以再维护一个脏页位图 (dirty page bitmap)，利用它，Vacuum 线程可以只检查发生过改动的数据，用空间换时间。Background Vacuuming 被用于任意 Version Storage 的方案。

*Cooperative Cleaning*

还有一种做法是当 worker thread 查询数据时，顺便将不再使用物理数据版本删除：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6wT4DQo_NTjQlHPZKQ%2F-M6wXGSHr_jBwULYvCS-%2FScreen%20Shot%202020-05-10%20at%2011.02.47%20AM.jpg?alt=media\&token=ef2b5ced-4511-4311-a83a-d9d193e1bdb6)

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6wT4DQo_NTjQlHPZKQ%2F-M6wXMYQT-hXcMi6lAdd%2FScreen%20Shot%202020-05-10%20at%2011.03.10%20AM.jpg?alt=media\&token=c537aa64-4566-4def-9f2d-1e25c1df1324)

cooperative cleaning 只能用于使用 O2N 的 version chain 方案。

#### Transaction-Level GC

让每个事务都保存着它的读写数据集合 (read/write set)，当 DBMS 决定什么时候这个事务创建的各版本数据可以被回收时，就按照集合内部的数据处理即可。

### Index Management

#### Primary Key Index

主键索引直接指向 version chain 的头部。

#### Secondary Indexes

二级索引有两种方式指向数据本身：

Approach #1：逻辑指针，即存储主键值或 Tuple Id\
Approach #2：物理指针，即存储指向 version chain 头部的指针

Physical Pointer

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6wT4DQo_NTjQlHPZKQ%2F-M6w_2HjMbwcNkm45C4m%2FScreen%20Shot%202020-05-10%20at%2011.14.49%20AM.jpg?alt=media\&token=500014eb-9b2e-4c03-8a89-6cdf1ec613cd)

Logical Pointer by Primary Key

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6wT4DQo_NTjQlHPZKQ%2F-M6w_EdXmLHUi5oJanfC%2FScreen%20Shot%202020-05-10%20at%2011.15.46%20AM.jpg?alt=media\&token=8c7e28a3-e170-4ec7-a67e-574d6aad7173)

Logical Pointer by Tuple Id

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6wT4DQo_NTjQlHPZKQ%2F-M6w_Rj3SflbBlFymMW9%2FScreen%20Shot%202020-05-10%20at%2011.16.40%20AM.jpg?alt=media\&token=753e01fb-6f94-412f-b217-dac298bc1e9b)

### MVCC Implementations

市面上 MVCC 的实现所做的设计决定如下表所示：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-M6wT4DQo_NTjQlHPZKQ%2F-M6w_lUwVPCHxUHV6We4%2FScreen%20Shot%202020-05-10%20at%2011.18.00%20AM.jpg?alt=media\&token=96493100-ee41-41bc-b4ce-e0cbe813fcfe)

## Conclusion

MVCC 被许多 DBMS 采用，即使那些不支持多语句事务 (multi-statement txns) 的 DBMS 也会使用这种方案，如一些 NoSQL 项目。

&#x20;
