Lecture#18 Multi-Version Concurrency Control

发布时间 2023-04-20 05:45:56作者: Angelia-Wang

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。

image-20230420035049467

\(T_1\)\(A_0\) 而不是 \(A_1\),因为 \(A_1\) 还没提交是个脏数据,若读 \(A_1\)\(T_2\) abort,则 \(T_1\) 要级联回滚;若 \(T_1\) 已提交后 \(T_2\) abort,会造成 not recoverable。

?这个例子是 Oracle的 Snopshot Isolation 级别,不是 Serializable。Serializable 无法光靠 MVCC 实现,要和其他的并发控制协议 (2PL、T/O、OCC) 结合实现。

image-20230420040034814

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

MVCC 的组成部分包括:

  1. 并发控制协议 (Concurrency Control Protocol)
  2. 版本存储 (Version Storage)
  3. 垃圾回收 (Garbage Collection)
  4. 索引管理 (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

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

image-20230420041302326

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

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 中最近的旧版本。

image-20230420042534032

2.3 Delta Storage

类似 Time-Travel Storage,但不存储整个 tuple,仅存储变化的字段。与 Time-Travel Storage 相比,写入更快,但读取速度较慢。

每次更新数据,仅将变化的字段信息存储到 delta storage segment 中,并更新指针。

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

image-20230420042940478

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 守护线程周期性地检查每条数据的不同版本,若它的结束时间小于当前活跃事务的最小时间戳,则将其删除。

image-20230420045304716

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

image-20230420045359173

2️⃣ Cooperative Cleaning:当 worker thread 查询数据时,顺便将不再使用物理数据版本删除。

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

image-20230420045657276

3.2 Transaction-level GC

让每个事务都保存着它的读写数据集合 (read/write set),DBMS 决定已完成的事务所创建的所有版本何时回收。

可能仍需要多个线程才能以足够快的速度为工作负载回收内存。

image-20230420050307195

4 Index Management

4.1 Primary Key Index

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

  • DBMS 必须多久更新一次 primary key 索引,取决于系统是否在更新元组时创建新版本
  • 如果一个事务更新了一个元组的 primary key 属性,那么这将被视为一个DELETE,然后 INSERT
image-20230420050938304

4.2 Secondary Indexes

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

1️⃣ 逻辑指针,即存储 Primary Key 或 Tuple Id。

image-20230420051307030

2️⃣ 物理指针,即存储指向 version chain 头部的指针。

二级索引不止一个,若都存物理地址,一旦该数据变为一个新版本,则所有二级索引的指针都要改。

image-20230420051121018

5 MVCC Indexes

MVCC DBMS 索引(通常)不存储有关带有键的元组的版本信息。 例外:索引组织表 (例如 MySQL)

  • 每个索引都必须支持 duplicated keys,因为在不同的快照中,相同的 key 可以指向不同的逻辑元组。
  • 每个索引的底层数据结构都必须支持 non-unique key 的存储。
  • 使用额外的执行逻辑为 primary key / unique index 执行条件插入:原子地检查 key是否存在,然后插入。
  • worker 可能会为一次提取返回多个条目。 然后,他们必须按照指针找到正确的物理版本。

MVCC duplicated key 存在问题:仅当逻辑删除的元组的所有版本都不可见时,DBMS 才会从数据库中物理删除元组。被逻辑删除的 tuple 会造成混乱。

image-20230420053014840

因此我们需要一种方法来表示元组已在某个时间点被逻辑删除。

Approach #1:Delete Flag:维护一个标志以指示逻辑元组在最新物理版本之后已被删除。该标志可放在元组标题或单独的列中。

Approach #2:Tombstone Tuple:建一个空的物理版本,表示删除了一个逻辑元组。为 tombstone tuple 使用单独的 pool,version chain pointer 中只有一个特殊的位模式,以减少存储开销。

6 MVCC 实现

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

image-20230420052431856

MVCC 被许多 DBMS 采用,即使那些不支持多语句事务 (multi-statement txns) 的 DBMS 也会使用这种方案,e.g., NoSQL。

参考链接

slides, notes

Multi-Version Concurrency Control

【cmu15-445】9.多版本并发控制