1. 背景与核心问题

MVCC(Multi-Version Concurrency Control,多版本并发控制)的核心目标是:

让不同事务在并发访问同一条逻辑记录时,能够看到对自己正确且一致的版本。

从存储引擎实现角度看,MVCC 最关键的是回答四个问题:

  1. 当前版本存放在哪里;
  2. 旧版本存放在哪里;
  3. 查询时如何找到对当前事务可见的版本;
  4. 旧版本如何被清理与回收。

不同的数据结构与存储引擎,本质上就是对这四个问题给出了不同答案。


2. B+Tree 与 MVCC

2.1 总体思路

在传统基于 B+Tree 的事务型存储引擎中,最典型的设计是:

B+Tree 负责定位记录,MVCC 负责决定当前事务应该看到哪个版本。

其核心特点是:

  • B+Tree 中主要维护当前版本
  • 旧版本不直接存放在 B+Tree 中
  • 旧版本通过 undo/version chain 保存
  • 查询时先找到最新版本,不可见时再向历史版本回退

这是一种典型的“当前态主存 + 历史版本外置”方案。


2.2 数据组织方式

这种方案通常可以抽象为如下结构:

B+Tree 叶子节点
   -> 当前记录
      -> 版本元数据(如事务 ID、删除标记等)
      -> 指向旧版本的指针
             -> undo record 1
                    -> undo record 2
                           -> undo record 3

在这种结构下:

  • B+Tree 叶子页中存放的是当前记录;
  • 当前记录上带有一些隐藏的版本信息;
  • 当前记录通过一个指针与旧版本链连接;
  • 历史版本通常位于独立的 undo / version store 区域中。

2.3 查询流程

B+Tree 与 MVCC 的查询配合过程大致如下:

第一步:通过 B+Tree 找到当前版本

对于主键点查、范围扫描、二级索引检索等,B+Tree 先定位出当前记录。

第二步:检查当前版本是否可见

系统根据当前记录携带的版本信息,与事务的快照规则进行比较,判断该版本是否对当前事务可见。

第三步:必要时沿版本链向后回退

如果当前版本不可见,则根据记录中的指针找到 undo 中的上一个版本,在内存中恢复旧版本,再继续做可见性判断。

这个过程会持续进行,直到:

  • 找到第一个可见版本;
  • 或者版本链结束。

2.4 这种设计的优点

1. 当前版本访问快

由于 B+Tree 中主要只维护当前版本,因此:

  • 索引结构更紧凑;
  • 页利用率更高;
  • 缓存命中率更好;
  • 范围扫描性能更稳定。

2. 多版本成本按需支付

并不是所有查询都要访问历史版本。只有在快照读或旧事务读取时,才需要沿版本链回退。

3. 很适合传统 OLTP

大多数 OLTP 系统读的都是最新已提交值,因此把主索引优化为“面向当前态”非常合理。


2.5 这种设计的代价

1. 历史版本读取可能较慢

如果某条记录被频繁更新,而又存在较老的事务快照,那么读取旧版本时可能需要回退很多步。

2. 需要额外的 purge / GC

历史版本不能永久存在,需要在确认不再被任何活跃事务访问后才能清理。

3. 长事务会阻碍版本清理

只要旧快照仍然存活,对应的旧版本就不能回收,容易导致 undo 膨胀。


2.6 适用场景

B+Tree + undo/version chain 的方案更适合:

  • 传统关系型 OLTP;
  • 大量点查和范围查询;
  • 二级索引访问频繁;
  • 读请求多数面向当前值;
  • 希望主索引保持紧凑的系统。

2.7 小结

B+Tree 与 MVCC 的典型结合方式是:主索引维护当前版本,历史版本外置到 undo/version chain,查询时先查当前版本,不可见再回退。


3. LSM-Tree 与 MVCC

3.1 总体思路

对于基于 LSM-Tree 的存储系统,MVCC 的常见实现方式与 B+Tree 明显不同。

这类系统通常不是:

  • 主存只保存最新版本;
  • 历史版本放在单独的 undo 链中;

而更常见的是:

把多个版本直接存入底层 LSM/KV 存储中,版本本身就是存储结构的一部分。

也就是说,LSM 路线更接近:

  • 同一个逻辑 key 对应多个物理版本;
  • 不同版本通过时间戳或版本号区分;
  • 查询时直接从多个版本中选择一个可见版本。

3.2 为什么 LSM 天然适合多版本

LSM-Tree 的基本特点是:

  • 写入偏追加;
  • 不强调原地更新;
  • 倾向于顺序写与后台合并;
  • 通过 compaction 整理数据。

而 MVCC 恰好需要“保留旧版本、写入新版本”。 因此对于 LSM 来说,一个非常自然的做法就是:

  • 不覆盖旧值;
  • 直接写入一个新版本;
  • 旧版本先保留;
  • 后续由 GC / compaction 清理。

所以在 LSM 中,“多版本直接进入主存储层”是非常顺手的设计。


3.3 数据组织方式

可以将 LSM 中的多版本结构抽象为:

k@12 -> value_v12
k@9  -> value_v9
k@5  -> value_v5

其中:

  • k 表示逻辑 key;
  • @12@9@5 表示不同版本时间戳。

这样,同一个 key 的多个版本会在排序上彼此相邻或便于遍历。


3.4 查询流程

LSM 中的 MVCC 查询流程通常可以概括为:

第一步:定位到该 key 的版本集合

通过 LSM 的查找与迭代机制,找到同一个 user key 的多个版本。

第二步:根据读时间戳选择可见版本

从这些版本中选出满足:

  • 版本时间戳不大于当前读时间;
  • 且满足事务快照规则;

的那个版本。

第三步:返回该版本

因此,LSM 中读取历史版本通常不是“回退构造”,而是“从已有多个版本中选一个”。


3.5 这种设计的优点

1. 版本管理天然适配分布式事务

如果系统本身就是基于时间戳推进事务,那么多版本直接做进 KV 存储层非常自然。

2. 历史读与时间旅行查询更方便

由于历史版本本来就存在主存储中,因此:

  • stale read;
  • 历史快照读;
  • CDC;
  • time-travel query;

都会更自然。

3. 更新路径简单

更新不需要像页式引擎那样维护外部 undo 回退链,而是直接写新版本即可。


3.6 这种设计的代价

1. 多版本扫描会带来读放大

如果某个 key 版本很多,读取时可能需要检查多个历史版本。

2. 更依赖 GC 与 compaction

旧版本长期积累会带来明显的空间与查询代价,因此垃圾回收与压缩合并非常关键。

3. 热点 key 更容易产生 MVCC amplification

高频更新的单行或单 key 会积累大量历史版本,影响读取效率。


3.7 适用场景

LSM + 多版本的方案更适合:

  • 分布式 KV / NewSQL;
  • 基于时间戳的事务系统;
  • stale read / snapshot read 使用频繁;
  • 追加写多、更新频繁;
  • 系统能接受依赖 compaction 与版本 GC 的架构。

3.8 小结

LSM-Tree 与 MVCC 的典型结合方式是:多个版本直接存入 LSM/KV,查询时按时间戳选择可见版本,而不是依赖外部 undo 链回退。


4. Bw-Tree 与 MVCC

4.1 一个容易混淆的前提

Bw-Tree 中有一个非常重要的概念:delta chain

但必须明确:

Bw-Tree 的 delta chain 通常不是 MVCC 版本链。

Bw-Tree 中的 delta chain 主要用于描述索引节点本身的增量更新,例如:

  • insert;
  • delete;
  • split;
  • merge;
  • 节点状态变更。

它体现的是:

  • 索引节点结构的演化过程

而不是:

  • 记录内容版本的演化过程

因此,Bw-Tree 中“节点 delta 链”和“事务版本链”是两个不同层面的概念。


4.2 Bw-Tree 与 MVCC 的自然分工

Bw-Tree 更适合作为一种:

  • 高并发;
  • latch-free;
  • 节点不原地更新;
  • 基于 mapping table 与 CAS 的索引结构。

而 MVCC 更关注的是:

  • 记录级别的多版本;
  • 事务时间戳可见性;
  • 旧版本回收。

所以 Bw-Tree 与 MVCC 最自然的组合方式通常是:

Bw-Tree 负责索引,记录层负责多版本。

换句话说:

  • Bw-Tree 叶子节点中通常存放的是记录指针;
  • 记录本身是多版本对象;
  • 查询时先通过 Bw-Tree 找到记录,再根据版本时间戳选择可见版本。

4.3 数据组织方式

可以抽象为如下结构:

Bw-Tree
   -> leaf item
      -> pointer to versioned record
             -> version 1 [begin, end)
             -> version 2 [begin, end)
             -> version 3 [begin, end)

其中:

  • beginend 表示该版本的有效时间区间;
  • 一个读事务只需检查其读时间戳是否落入该区间。

判断规则通常可以抽象为:

begin <= read_ts < end

4.4 查询流程

Bw-Tree 与 MVCC 结合后的查询过程通常为:

第一步:通过 Bw-Tree 定位到候选记录

Bw-Tree 提供有序索引能力与高并发更新能力。

第二步:根据记录版本元数据判断可见性

记录层维护多个版本,每个版本带有 begin/end timestamp 或等价的版本信息。

第三步:返回对当前事务可见的版本

查询不一定需要像 undo 链那样逐层“回退恢复”,而是直接在记录层的多版本中判断可见性。


4.5 这种设计的优点

1. 索引层与事务层职责清晰

  • Bw-Tree 负责高并发索引管理;
  • 记录层负责版本管理与可见性控制。

2. 适合内存优化设计

Bw-Tree 非常强调 latch-free / lock-free 风格,与内存优化引擎结合自然。

3. 避免将 MVCC 强行塞入索引节点更新链

索引的节点增量更新与记录版本历史分层管理,结构更加清晰。


4.6 这种设计的代价

1. 系统实现复杂度较高

需要同时管理:

  • 索引层的 delta chain;
  • 记录层的版本对象;
  • 版本 GC;
  • 节点 consolidation。

2. 更适合特定体系结构

Bw-Tree 并不是传统磁盘页式 OLTP 的主流选择,它更多出现在特定的高并发内存优化场景中。


4.7 工业界代表

Bw-Tree 与 MVCC 结合的典型工业实现是:

  • Microsoft SQL Server In-Memory OLTP(Hekaton)

其设计特点可以总结为:

  • Bw-Tree 用作范围索引;
  • 记录层维护多版本;
  • 更新创建新版本;
  • 可见性通过 begin/end timestamp 判断;
  • 旧版本由后台 GC 清理。

因此,Hekaton 更接近:

Bw-Tree + row-version MVCC

而不是:

Bw-Tree + undo 链


4.8 小结

Bw-Tree 与 MVCC 的典型结合方式是:Bw-Tree 只做高并发索引,记录层负责多版本,查询时先通过索引找到记录,再根据版本时间戳判断可见性。


5. 三种路线的本质对比

5.1 从“旧版本存放位置”看

B+Tree

  • 当前版本在主索引 / 主记录中;
  • 旧版本在 undo / version chain 中。

LSM-Tree

  • 当前版本与旧版本都直接存在 LSM/KV 主存储中;
  • 版本通过时间戳区分。

Bw-Tree

  • Bw-Tree 主要负责索引;
  • 旧版本保存在记录层的多版本对象中。

5.2 从“读取旧版本方式”看

B+Tree

  • 先定位当前版本;
  • 当前版本不可见时,沿 undo 链回退。

LSM-Tree

  • 直接在多个已存储版本中,按时间戳选一个可见版本。

Bw-Tree

  • 先通过索引找到记录;
  • 再在记录层依据 begin/end timestamp 选择版本。

5.3 从“设计哲学”看

B+Tree

更像是:

当前态主存 + 历史外置

LSM-Tree

更像是:

历史版本本来就是存储层的一部分

Bw-Tree

更像是:

索引层和版本层分离,索引负责定位,版本层负责 MVCC


6. 应用场景对比

6.1 B+Tree + undo/version chain

更适合:

  • 传统关系型 OLTP;
  • 高效点查、范围扫;
  • 二级索引密集使用;
  • 大量查询读取当前值。

6.2 LSM + 多版本 key

更适合:

  • 分布式 KV / NewSQL;
  • 时间戳驱动的事务模型;
  • 历史读、stale read、CDC 需求较多;
  • 高写入吞吐与追加写友好场景。

6.3 Bw-Tree + row-version

更适合:

  • 内存优化存储引擎;
  • 高并发索引更新;
  • latch-free / lock-free 风格实现;
  • 索引与事务版本管理分层的体系。

7. 最终总结

从本质上看,B+Tree、LSM-Tree、Bw-Tree 与 MVCC 的结合方式可以概括为:

B+Tree

主索引维护当前版本,旧版本外置,查询时先查当前版本,不可见再回退。

LSM-Tree

多个版本直接进入存储层,查询时按时间戳选择一个可见版本。

Bw-Tree

Bw-Tree 负责高并发索引,记录层维护多版本,查询时通过索引定位后再做版本可见性判断。

如果只记一个总判断,可以记为:

B+Tree 强调“当前态索引 + 历史外置”,LSM 强调“多版本就是主存储的一部分”,Bw-Tree 强调“索引层与版本层分离”。

8. 延伸思考

理解各类索引结构与 MVCC 的结合方式,不仅仅是为了回答"旧版本存在哪里"这一个问题,而是帮助我们建立对存储引擎整体设计的直觉:

  • 写入路径决定了版本的存放形式——原地更新的 B+Tree 需要外置 undo,追加写的 LSM 可以直接将多版本落入主存储;
  • 读取路径决定了可见性判断的代价——undo 链的逐步回退 vs. 多版本的一次定位选择;
  • GC 策略决定了旧版本何时能被安全清理——长事务与 GC 之间的张力在三种方案中都必须面对。

不同的工程取舍背后,其实都是对同一组核心问题的不同回答。在设计或评估一个存储引擎时,从这四个维度出发往往能很快抓住其 MVCC 实现的本质。