Lecture#18 Multi-Version Concurrency Control
MVCC 不是并发控制的 (继 2PL、时间戳控制协议) 第三种实现,它不单独作为一种并发控制的实现,而是与 2PL / 时间戳控制协议 (T/O、OCC) 结合使用。
多版本并发控制 (MVCC) 首次被提出是在 1978 年的一篇 MIT 的博士论文中。在 80 年代早期,DEC 的 Rdb/VMS 和 InterBase 首次真正实现了 MVCC,其作者是 Jim Starkey,NuoDB 的联合创始人。如今,Rdb/VMS 成了 Oracle Rdb,InterBase 成为开源项目 Firebird。
1 Multi-Version Concurrency Control
简言之,实现 MVCC 的 DBMS 在内部维持着单个逻辑数据的多个物理版本:
- 当事务修改某数据时,DBMS 将为其创建一个新的版本;
- 当事务读取某数据时,它将读到该数据在事务开始时刻之前的最新版本。
MVCC 的核心优势可以总结为以下两句话:
- Writers don't block readers. 写不阻塞读
- Readers don't block writers. 读不阻塞写
只读事务无需加锁就可以读取数据库某一时刻的快照 (使用时间戳来决定版本),如果保留数据的所有历史版本,DBMS 甚至能够支持读取任意历史版本的数据,即 time-travel queries。
🌰
读 而不是 ,因为 还没提交是个脏数据,若读 后 abort,则 要级联回滚;若 已提交后 abort,会造成 not recoverable。
👇这个例子是 Oracle的 Snopshot Isolation 级别,不是 Serializable。Serializable 无法光靠 MVCC 实现,要和其他的并发控制协议 (2PL、T/O、OCC) 结合实现。

MVCC 并不只是一个并发控制协议,并发控制协议只是它的一个组成部分。它深刻地影响了 DBMS 管理事务和数据的方式。
MVCC 的组成部分包括:
- 并发控制协议 (Concurrency Control Protocol)
- 版本存储 (Version Storage)
- 垃圾回收 (Garbage Collection)
- 索引管理 (Index Management)
每一部分都可以选择不同的方案,可以根据具体场景作出最优的设计选择。
2 Concurrency Control Protocol
前面 2 节课已经介绍了各种并发控制协议,MVCC 可以选择其中任意一个:
Approach #1:Two-Phase Locking (2PL):按照 2PL 的约定获取和释放锁
Approach #2:Timestamp Ordering (T/O):为每个事务赋予时间戳,并用以决定执行顺序
Approach #3:Optimistic Concurrency Control (OCC):为每个事务创建 private workspace,并将事务分为 read, validate, write 三个阶段处理
2 Version Storage
如何存储一条数据的多个版本?DBMS 通常会在每条数据上拉一条版本链表 (version chain),所有相关的索引都会指到这个链表的 head,DBMS 可以利用它找到一个事务应该访问到的版本。
不同的版本存储方案在 version chain 上存储的数据不同,主要有 3 种存储方案:
Approach #1:Append-Only Storage:新版本通过追加的方式存储在同一张表中
Approach #2:Time-Travel Storage:老版本被复制到单独的一张表中
Approach #3:Delta Storage:老版本数据的被修改的字段值被复制到一张单独的增量表 (delta record space) 中
2.1 Append-Only Storage
同一个逻辑数据的所有物理版本都被存储在同一张表上,每次更新时,就往表上追加一个新的版本记录,并在旧版本的数据上增加一个指针指向新版本。

也许你已经注意到,指针的方向也可以从新到旧,二者的权衡如下:
Approach #1:Oldest-to-Newest (O2N):写的时候追加即可,读的时候需要遍历链表
Approach #2:Newest-to-Oldest (N2O):写的时候需要更新所有索引指针,读的时候不需要遍历链表
2.2 Time-Travel Storage
单独拿一张表 (Time-Travel Table) 来存历史 tuples 数据。
更新时,把 tuple 的当前旧版本复制到 TTT 中,用新数据覆盖主表中的 tuple,并更新主表中 tuple 的指针,指向 TTT 中最近的旧版本。

2.3 Delta Storage
类似 Time-Travel Storage,但不存储整个 tuple,仅存储变化的字段。与 Time-Travel Storage 相比,写入更快,但读取速度较慢。
每次更新数据,仅将变化的字段信息存储到 delta storage segment 中,并更新指针。
DBMS 可以通过 delta 数据逆向恢复数据到之前的版本。

3 Garbage Collection
随着时间的推移,DBMS 中数据的旧版本可能不再会被用到,如:
- 已经没有活跃的事务需要看到该版本
- 该版本是被一个已经中止的事务创建
这时候 DBMS 需要删除这些可以回收的物理版本,这个过程也被称为 GC。在 GC 的过程中,还有两个附加设计决定:
- 如何查找过期的数据版本
- 如何确定某版本数据能被安全回收
GC 可以从两个角度出发:
Approach #1:Tuple-level:直接检查每条数据的旧版本数据
Approach #2:Transaction-level:每个事务负责跟踪数据的旧版本,DBMS 不需要亲自检查单条数据
3.1 Tuple-Level GC
也有两种实现方法:
1️⃣ Background Vacuuming:单独的 Vacuum 守护线程周期性地检查每条数据的不同版本,若它的结束时间小于当前活跃事务的最小时间戳,则将其删除。

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

2️⃣ Cooperative Cleaning:当 worker thread 查询数据时,顺便将不再使用物理数据版本删除。
只能用于使用 O2N 的 version chain 方案。

3.2 Transaction-level GC
让每个事务都保存着它的读写数据集合 (read/write set),DBMS 决定已完成的事务所创建的所有版本何时回收。
可能仍需要多个线程才能以足够快的速度为工作负载回收内存。

4 Index Management
4.1 Primary Key Index
主键索引直接指向 version chain 的头部。
- DBMS 必须多久更新一次 primary key 索引,取决于系统是否在更新元组时创建新版本
- 如果一个事务更新了一个元组的 primary key 属性,那么这将被视为一个DELETE,然后 INSERT

4.2 Secondary Indexes
二级索引有两种方式指向数据本身:
1️⃣ 逻辑指针,即存储 Primary Key 或 Tuple Id。

2️⃣ 物理指针,即存储指向 version chain 头部的指针。
二级索引不止一个,若都存物理地址,一旦该数据变为一个新版本,则所有二级索引的指针都要改。

5 MVCC Indexes
MVCC DBMS 索引(通常)不存储有关带有键的元组的版本信息。 例外:索引组织表 (例如 MySQL)
- 每个索引都必须支持 duplicated keys,因为在不同的快照中,相同的 key 可以指向不同的逻辑元组。
- 每个索引的底层数据结构都必须支持 non-unique key 的存储。
- 使用额外的执行逻辑为 primary key / unique index 执行条件插入:原子地检查 key 是否存在,然后插入。
- worker 可能会为一次提取返回多个条目。 然后,他们必须按照指针找到正确的物理版本。
MVCC duplicated key 存在问题:仅当逻辑删除的元组的所有版本都不可见时,DBMS 才会从数据库中物理删除元组。被逻辑删除的 tuple 会造成混乱。

因此我们需要一种方法来表示元组已在某个时间点被逻辑删除。
Approach #1:Delete Flag:维护一个标志以指示逻辑元组在最新物理版本之后已被删除。该标志可放在元组标题或单独的列中。
Approach #2:Tombstone Tuple:建一个空的物理版本,表示删除了一个逻辑元组。为 tombstone tuple 使用单独的 pool,version chain pointer 中只有一个特殊的位模式,以减少存储开销。
6 MVCC 实现
市面上 MVCC 的实现所做的设计决定如下表所示:

MVCC 被许多 DBMS 采用,即使那些不支持多语句事务 (multi-statement txns) 的 DBMS 也会使用这种方案,e.g., NoSQL。
参考链接
🌟 浅析数据库并发控制 这个一定要看下,将 2PL,OOC,MVCC 串起来理解,有助于知识体系架构的建立。
本文作者:Joey-Wang
本文链接:https://www.cnblogs.com/joey-wang/p/17335310.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步