土法炼钢兴趣小组的算法知识备份

【RocksDB 内核机制】Get 与 Snapshot:SuperVersion 与 sequence number

文章导航

分类入口
databasestorage
标签入口
#rocksdb#get#snapshot#superversion#sequence-number#mvcc#lsm-tree#lookup-key

目录

第 6 篇 把 SST 磁盘布局与 VersionSet/MANIFEST 钉住了;读者已经知道 LSM 树每一层有哪些 SST 文件。生产里更常问的是:一次 Get(key) 究竟按什么顺序查 MemTable、Immutable MemTable 和 L0–Ln?ReadOptions::snapshot 如何截断可见版本?flush/compaction 进行中,读线程会不会看到半套状态?

本文是系列 第 7 篇,沿 DBImpl::GetImpl(RocksDB 9.xdb/db_impl/db_impl.cc)与 SnapshotImpldb/snapshot_impl.h)把读路径钉住。与 lsm-tree DIY 第 2 篇 的 toy Get 不同,RocksDB 用 SuperVersion 把 mem/imm/version 绑成一致快照,用 sequence number 做版本裁剪。

版本锚定:RocksDB 9.xreproduce/rocksdb/ROCKSDB_MAJOR = 9)。本文不粘贴未实测的 db_bench 延迟数字。


一、点查在 LSM 上的层级模型

LSM 写路径把更新追加到 MemTable,flush 后落入 L0,再经 compaction 沉到 L1+。读路径必须在 所有仍可能持有该 user key 最新版本 的结构上查找,并按 版本从新到旧 取第一个对当前读者可见的记录。

flowchart TB
  API["DB::Get / GetImpl"] --> SV["GetAndRefSuperVersion"]
  SV --> MEM["mem->Get"]
  MEM -->|miss| IMM["imm->Get"]
  IMM -->|miss| VER["Version::Get"]
  VER --> FP["FilePicker 逐文件"]
  FP --> TC["TableCache::Get"]
  TC --> SST["BlockBasedTable::Get"]
阶段 数据结构 文件内查找 层间 key 范围
1 可变 MemTable SkipList 全量
2 Immutable MemTable 队列 多个 SkipList,新→旧 全量
3 Version(L0–Ln) 每文件 InternalIterator / GetContext L0 重叠;L1+ 同层不重叠

MemTable 命中 时直接返回,不再碰磁盘;MemTable 未命中 才进入 Version::Get,由 FilePickerstorage_info_.level_files_brief_file_indexer_ 决定候选 SST,再经 TableCache::Get 打开单个文件做点查(见 db/version_set.cc Version::Get)。

与 B-Tree 引擎「树高固定、页内 MVCC 链」不同,LSM 的读放大来自 层数 × 文件数;第 9 篇讲 Block Cache / Bloom 如何裁剪无效 I/O。


二、Internal Key 与 sequence number

RocksDB 磁盘与 MemTable 存的不是 user key,而是 Internal Keyuser_key + 8 字节 trailer,trailer 打包 sequence number(高 56 bit)ValueType(低 8 bit)db/dbformat.h)。

// db/dbformat.h(节选)
enum ValueType : unsigned char {
  kTypeDeletion = 0x0,
  kTypeValue = 0x1,
  kTypeMerge = 0x2,
  // ...
  kTypeSingleDeletion = 0x7,
  kTypeRangeDeletion = 0xF,  // meta block
};

inline uint64_t PackSequenceAndType(uint64_t seq, ValueType t) {
  assert(seq <= kMaxSequenceNumber);
  return (seq << 8) | t;
}

排序规则InternalKeyComparator 先比 user key(含 user-defined timestamp 时按 comparator 规则),user key 相同时 sequence 大的排在前——因此同一 user key 的多版本在 SST / MemTable 中 降序 排列,点查时 先遇到最新写入

每次 Write 为 batch 内每条记录分配递增 sequence(VersionSet::AllocateSequence 路径);删除写 kTypeDeletion / kTypeSingleDeletion,不是原地抹掉旧值。Compaction 在 无 Snapshot 引用 且 tombstone 不再被更旧版本需要时,才物理删除(第 10 篇展开)。

LookupKeydb/lookup_key.h)在 GetImpl 入口构造:把 user key 与 snapshot 对应的 sequence 编码成 MemTable 键和 InternalIterator _seek 键,保证各层用同一可见上界:

// db/lookup_key.h(接口)
LookupKey(const Slice& _user_key, SequenceNumber sequence,
          const Slice* ts = nullptr);
Slice memtable_key() const;
Slice internal_key() const;

三、GetImpl:从 SuperVersion 到 Version::Get

DBImpl::GetImplDB::Get / MultiGet / 事务读的核心实现(db/db_impl/db_impl.cc)。主干步骤:

  1. GetAndRefSuperVersion(cfd) — 取得当前 CF 的 SuperVersion 并增加引用计数。
  2. 确定 snapshot sequence — 见下一节。
  3. LookupKey lkey(key, snapshot, read_options.timestamp)
  4. sv->mem->Getsv->imm->Get — 命中则 done = true
  5. sv->current->Get(...) — 在 Version 上走 FilePicker + TableCache::Get
  6. ReturnAndCleanupSuperVersion — 释放 SuperVersion 引用。
// db/db_impl/db_impl.cc(节选,顺序与语义一致)
SuperVersion* sv = GetAndRefSuperVersion(cfd);

SequenceNumber snapshot;
if (read_options.snapshot != nullptr) {
  snapshot = static_cast<const SnapshotImpl*>(read_options.snapshot)->number_;
} else {
  snapshot = GetLastPublishedSequence();
  // WriteUnprepared 等 callback 可能抬高 max_visible_seq
}

LookupKey lkey(key, snapshot, read_options.timestamp);

if (sv->mem->Get(lkey, ...)) {
  done = true;
} else if (sv->imm->Get(lkey, ...)) {
  done = true;
}

if (!done) {
  sv->current->Get(read_options, lkey, ...);
}
ReturnAndCleanupSuperVersion(cfd, sv);

Version::Get 内循环(db/version_set.cc):

FilePicker fp(user_key, ikey, &storage_info_.level_files_brief_, ...);
FdWithKeyRange* f = fp.GetNextFile();
while (f != nullptr) {
  if (*max_covering_tombstone_seq > 0) break;  // range tombstone 已覆盖
  *status = table_cache_->Get(read_options, *internal_comparator(),
                               *f->file_metadata, ikey, &get_context, ...);
  switch (get_context.State()) {
    case GetContext::kFound: /* 返回 */ break;
    case GetContext::kDeleted: /* key 存在但已删 */ break;
    case GetContext::kNotFound: f = fp.GetNextFile(); break;
    // ...
  }
}

GetContext::SaveValuetable/get_context.cc)在 Internal Key 匹配 user key 后,用 CheckCallback(parsed_key.sequence) 判断 该版本是否 ≤ snapshot;不满足则 continue 找更旧版本。遇到 kTypeDeletion 且可见则置 kDeleted,对调用方表现为 NotFound


四、SuperVersion:读一致性的锚点

SuperVersiondb/column_family.h)把一次读需要的 四层指针 绑在一起:

struct SuperVersion {
  ColumnFamilyData* cfd;
  ReadOnlyMemTable* mem;           // 当前可变 MemTable
  MemTableListVersion* imm;        // immutable MemTable 列表快照
  Version* current;                // 当前 LSM Version(SST 集合)
  MutableCFOptions mutable_cf_options;
  uint64_t version_number;
  // ...
  std::atomic<uint32_t> refs;
};

何时新建 SuperVersion:MemTable 写满切换、flush 完成、compaction 安装新 Version、部分 CF 选项变更等——写线程在持 mutex_InstallSuperVersion,旧 SuperVersion 引用计数归零后异步 Cleanup

读线程如何拿 SuperVersionGetAndRefSuperVersioncfd->GetThreadLocalSuperVersion(this)db_impl.cc)。线程本地缓存 SuperVersion 指针,避免每次 Get 抢 DB mutex;失效时从 ColumnFamilyData::super_version_ 重新 ref。

这保证 Get 全程看到的 mem / imm / current 来自同一安装点,不会在 MemTable 查完、SST 查一半时混入另一轮 flush 的新文件。代价是 flush/compaction 可能 延迟释放 仍被 SuperVersion 引用的 MemTable 和 SST(第 11 篇 write stall 与此相关)。

sequenceDiagram
  participant R as Read thread
  participant SV as SuperVersion
  participant W as Write / Flush thread
  R->>SV: Ref (GetAndRefSuperVersion)
  R->>SV: mem / imm / Version::Get
  W->>SV: InstallSuperVersion (new)
  Note over SV: old SV refs>0, 暂不 Cleanup
  R->>SV: Unref (ReturnAndCleanupSuperVersion)
  Note over SV: refs==0 → Cleanup mem/imm/version

五、Snapshot 语义与生命周期

公开 API(include/rocksdb/db.h):

SnapshotImpldb/snapshot_impl.h):

class SnapshotImpl : public Snapshot {
 public:
  SequenceNumber number_;  // const after creation
  SequenceNumber min_uncommitted_ = kMinUnCommittedSeq;  // WritePrepared 用
  SequenceNumber GetSequenceNumber() const override { return number_; }
  // ...
};

SnapshotList环形双向链表 维护所有活跃 snapshot;compaction 通过 GetSnapshotContext 收集 最老 snapshot sequence,决定哪些 tombstone / 旧版本 不能物理删除

场景 ReadOptions::snapshot 可见 sequence 上界
默认读 nullptr GetLastPublishedSequence()
显式快照 GetSnapshot() 返回值 SnapshotImpl::number_
写冲突边界 GetSnapshotForWriteConflictBoundary() 同上,且标记 is_write_conflict_boundary_

关键顺序GetImpl 注释):必须先 GetAndRefSuperVersion 再取 snapshot sequence(无显式 snapshot 时)。若顺序颠倒,中间发生的 flush 可能 compact 掉 本应对新读可见 的数据,或让 snapshot 读看到不一致结果。

ManagedSnapshotdb/snapshot_impl.cc)RAII 包装 GetSnapshot / ReleaseSnapshot,适合 C++ 嵌入代码。

与 PostgreSQL MVCC 的一句对照:PostgreSQL 用 事务 xmin/xmax + CLOG 在堆页上判断元组对快照是否可见;RocksDB 用 全局递增 sequence 编码在 Internal Key 尾部,读路径在合并序上 跳过 sequence > snapshot 的版本,不维护 per-key 版本链与 undo。


六、特殊路径与边界

Merge 与 Blob

Range Tombstone

MemTable / SST 除点 tombstone 外,还有 range deletion meta block。max_covering_tombstone_seq 非零时,Version::Get 提前终止 文件扫描——该 key 已被范围删除覆盖,无需再读更旧文件。

read_tier

ReadOptions::read_tier == kPersistedTier 且存在未持久化数据时,GetImpl 跳过 MemTableskip_memtable),只读已 flush SST——用于特殊一致性检查,非常规业务读。

Column Family

本文以单 CF 描述;多 CF 时每个 ColumnFamilyHandle 独立 SuperVersion 与 LSM 树,WAL 仍共享(第 13 篇)。


七、调试与统计

RocksDB Statistics 中与 Get 相关的 tick(GetImpl / Version::GetRecordTick):

Tick 含义
MEMTABLE_HIT / MEMTABLE_MISS 是否在 mem/imm 命中
GET_HIT_L0 / GET_HIT_L1 / GET_HIT_L2_AND_UP 在哪一层 SST 命中
NUMBER_KEYS_READ 读 key 计数
NUMBER_SUPERVERSION_RELEASES SuperVersion 释放次数

DB::GetProperty("rocksdb.num-files-at-levelN") 等需持锁或通过 SuperVersion 读 VersionGetIntPropertyOutOfMutex 路径)。排查「点查慢」时:先看 L0 文件数memtable miss 比例,再查 Block Cache(第 9 篇)。


八、小结


上一篇SST 格式与 MANIFEST

下一篇Iterator 与 MergeIterator

返回 系列目录


参考资料

  1. RocksDB 9.x 源码:db/db_impl/db_impl.ccGetImplGetAndRefSuperVersionGetSnapshotImplReleaseSnapshot);db/snapshot_impl.hdb/column_family.hSuperVersion);db/dbformat.hdb/lookup_key.hdb/version_set.ccVersion::Get);table/get_context.cc(A 级)。
  2. RocksDB Wiki, Basic Operations / Snapshots(A 级 API 语义)。
  3. 第 6 篇 SST 与 MANIFESTlsm-tree WAL+MemTable(DIY 对照)。
  4. PostgreSQL 内核 MVCC(PG 可见性一句对照,B 级阅读路径)。

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-07 · database / storage

【RocksDB 内核机制】Column Family:共享 WAL 与独立 LSM

从 RocksDB 3.0 引入的 Column Family 出发,拆解共享 WAL、独立 MemTable/SST/Version 链、`ColumnFamilyHandle` 生命周期与 DBOptions/ColumnFamilyOptions 分层;并对照 Flink 多 state 变量到 CF 的映射边界。


By .