kv数据库-leveldb (11) 版本集 (VersionSet / Version)

在上一章 合并 (Compaction) 中,我们学习了 LevelDB 是如何通过后台合并任务,来清理冗余数据、减少文件数量,从而保持数据库的高效和整洁。Compaction 过程会创建新的 排序字符串表 (SSTable) 文件,并废弃掉旧的文件。

这带来了一个关键问题:在任何一个时间点,数据库里都可能存在着大量的文件。当一个读请求进来时,LevelDB 如何精确地知道应该去查询哪些 SSTable 文件,又应该忽略哪些已经被废弃的文件呢?它需要一个权威的"文件清单"来指引它。

这个"文件清单"以及管理它的整个系统,就是我们本章要探讨的核心------VersionSetVersion

什么是 Version 和 VersionSet?

我们可以把整个数据库想象成一个大型图书馆。SSTable 文件就是书架上一本本的书。

  • Version (版本) :可以看作是图书馆在某个特定时刻的完整目录清单 。这份清单精确地记录了每一层书架(Level)上都有哪些书(SSTable 文件)。它是一个不可变的快照 。一旦生成,这份目录清单就不会再被修改。任何对图书馆藏书的变动(比如 Compaction 完成后),都会产生一份全新的目录清单。

  • VersionSet (版本集) :则是图书馆的总目录办公室 ,或者说,是管理所有目录清单的总图书管理员。它的职责是:

    1. 保管着当前最新、最有效 的那份目录清单(我们称之为 current 版本)。所有的读写操作都依赖这份最新清单。
    2. 管理所有历史版本的目录清单。因为可能还有读者(迭代器 (Iterator))正拿着一份旧清单在查阅,所以旧清单不能马上销毁。
    3. 负责"发布"新的目录清单。当藏书发生变化(Compaction 完成)时,它会根据变更信息,在当前清单的基础上生成一份新清单,并原子性地切换过去。

简而言之,Version 描述了数据库在某一刻的静态状态 ,而 VersionSet 则管理着这些状态的动态演变

状态的演变:VersionEdit

如果每次 Compaction 之后,我们都重新制作一份包含所有上千个文件信息的全新目录,那效率也太低了。一个更聪明的方法是只记录变化

  • VersionEdit (版本编辑) :这就是描述"变化"的对象。它就像一张小小的变更便签,上面只写着:"在 Level 1 书架上,移除 了 001.sst 这本书,同时新增 了 005.sst 和 006.sst 这两本书"。VersionEdit 本身不包含数据库的全部状态,它非常轻量,只记录了从一个 Version 到下一个 Version 的增量变化。

这个机制和版本控制系统 Git 非常相似:

  • Version 就像一个 Git commit,代表了项目在某个时间点的完整快照。
  • VersionEdit 就像一个 Git diffpatch,描述了两次 commit 之间的具体变化。
  • VersionSet 就像是整个 .git 仓库 ,管理着所有的 commit 历史,并知道 HEAD 指向哪一个。

VersionSet 是如何工作的?

VersionSet 是 LevelDB 内部的管理者,我们无法直接操作它。但理解它的工作流程,是理解 LevelDB 一致性和恢复机制的关键。

应用一次变更

当一次 合并 (Compaction) 完成后,它不会直接修改文件系统,而是会生成一个 VersionEdit 对象,然后把它交给 VersionSetVersionSet 会执行一个被称为 LogAndApply 的关键操作。

sequenceDiagram participant Compaction as Compaction 线程 participant VersionSet as 版本集 participant MANIFEST as MANIFEST 文件 (磁盘) participant CurrentVersion as 当前 Version 对象 participant NewVersion as 新 Version 对象 Compaction ->> VersionSet: 1. LogAndApply(versionEdit) Note right of VersionSet: 收到变更请求 VersionSet ->> NewVersion: 2. 基于 CurrentVersion
应用 versionEdit
创建新版本 VersionSet ->> MANIFEST: 3. 将 versionEdit 内容
作为一条日志写入文件 Note right of MANIFEST: 保证状态变更不丢失 MANIFEST -->> VersionSet: 写入成功 VersionSet ->> VersionSet: 4. 原子地将 "current" 指针
从 CurrentVersion 切换到 NewVersion Note right of VersionSet: 数据库状态已更新 VersionSet -->> Compaction: 返回成功
  1. 生成 VersionEdit :Compaction 任务完成后,会创建一个 VersionEdit,记录下哪些文件被删除了,哪些新文件被添加了。
  2. 创建新 VersionVersionSet 接收到 VersionEdit后,它会以当前的 Version 为基础,应用这个 VersionEdit 描述的变更,在内存中创建一个全新的 Version 对象。
  3. 写入 MANIFEST 文件 :在切换到新版本之前,VersionSet 必须将这个 VersionEdit 持久化 到一个特殊的日志文件中,这个文件通常叫做 MANIFEST-xxxxxx。这和 预写日志 (Log / WAL) 的原理一样,确保了即使在状态切换时发生崩溃,重启后也能通过重放 MANIFEST 日志来恢复到正确的状态。
  4. 原子切换 :只有当 VersionEdit 成功写入 MANIFEST 文件后,VersionSet 才会把内部的 current_ 指针,从旧的 Version 对象切换到新的 Version 对象。这个切换是内存中的一个指针赋值操作,是原子性的。从这一刻起,所有新的数据库操作都将看到更新后的数据库状态。

提供读取快照

当一个读请求 Get 或一个新的 迭代器 (Iterator) 被创建时,它们会从 VersionSet 获取当前Version 并持有一个对它的引用。只要这个引用存在,VersionSet 就不会销毁这个 Version 对象以及它所代表的所有 SSTable 文件,即使这个 Version 后来被更新的版本所取代。

这确保了长时间运行的扫描操作能在一个一致的、不会改变的数据快照上进行,不受并发写入和 Compaction 的影响。

深入代码实现

让我们通过源码来具体看看这三个核心概念。

Version 类 (db/version_set.h)

Version 类的核心就是它包含的文件列表。它自身并没有复杂的逻辑,更像一个只读的数据容器。

cpp 复制代码
// 来自 db/version_set.h (简化后)
class Version {
 private:
  friend class Compaction;
  friend class VersionSet;

  // 每个层级的文件列表
  // files_[0] 是 Level-0 的文件元数据列表
  // files_[1] 是 Level-1 的文件元数据列表, 等等
  std::vector<FileMetaData*> files_[config::kNumLevels];

  // ... 其他用于 Compaction 决策的元数据 ...

  VersionSet* vset_;  // 指向所属的 VersionSet
  Version* next_;     // 在 VersionSet 的双向链表中的下一个版本
  Version* prev_;     // 上一个版本
  int refs_;          // 引用计数
};

可以看到,Version 的核心成员 files_ 是一个数组,每个元素是一个 FileMetaData 指针的 vector,代表了对应层级的所有 SSTable 文件。refs_ 字段是引用计数,用于管理 Version 对象的生命周期。

VersionEdit 类 (db/version_edit.h)

VersionEdit 则清晰地展示了它作为"变更描述"的角色。

cpp 复制代码
// 来自 db/version_edit.h (简化后)
class VersionEdit {
 private:
  friend class VersionSet;

  typedef std::set<std::pair<int, uint64_t>> DeletedFileSet;

  // ... 其他元数据变更,如 comparator, log_number ...

  // <level, file_number>
  DeletedFileSet deleted_files_; 
  // <level, FileMetaData>
  std::vector<std::pair<int, FileMetaData>> new_files_; 
};

它的主要成员 deleted_files_new_files_ 直接对应了我们之前"变更便签"的比喻,一个记录要删除的文件,一个记录要新增的文件。

VersionSet 类 (db/version_set.h)

VersionSet 是整个系统的中枢。它维护着一个 Version 对象的双向循环链表,以及指向当前版本的指针。

cpp 复制代码
// 来自 db/version_set.h (简化后)
class VersionSet {
 public:
  // 将 *edit 应用到当前版本,形成一个新版本
  // 并将变更内容写入 MANIFEST 日志
  Status LogAndApply(VersionEdit* edit, port::Mutex* mu);

  // 返回当前版本
  Version* current() const { return current_; }

  // ... 其他管理方法 ...

 private:
  // ...
  
  // MANIFEST 文件的写入器
  log::Writer* descriptor_log_;
  // 版本链表的头节点 (哨兵)
  Version dummy_versions_;  
  // 指向当前活跃的版本
  Version* current_;        
};
  • current_: 指向当前最新的 Version
  • dummy_versions_: 这是一个哨兵节点,它和它的 next_prev_ 指针构成了一个包含所有活跃 Version 对象的双向循环链表。current_ 总是等于 dummy_versions_.prev_
  • LogAndApply(): 这是外部世界(主要是 Compaction 线程)改变数据库元数据的唯一入口,它保证了状态变更的原子性和持久性。

数据库恢复

当 LevelDB 启动时,它会找到 CURRENT 文件,通过它定位到最新的 MANIFEST 文件。然后,它会读取 MANIFEST 文件中的所有 VersionEdit 记录,并在内存中从头到尾重放 这些变更,一步步地重建出数据库关闭前的最后一个 Version 状态。这个过程完成后,数据库就恢复到了一个一致的状态,可以开始对外提供服务了。

总结

在本章中,我们深入了解了 LevelDB 的状态管理核心------VersionSet

  • Version 是数据库在某个时间点的文件列表快照 ,它定义了哪些 SSTable 文件是有效的。
  • VersionEdit 是一个描述版本之间增量变化的轻量级对象,记录了文件的增删。
  • VersionSet 是所有 Version管理器 ,它负责维护当前版本,并通过应用 VersionEdit 来原子性地生成新版本。
  • 整个状态变更的过程被持久化地记录在 MANIFEST 文件中,这保证了数据库在崩溃后能够准确地恢复其元数据。

这个精巧的版本控制系统,是 LevelDB 能够实现读操作的一致性快照(MVCC 的一种形式)、安全的后台 Compaction 以及崩溃恢复能力的基础。

我们现在已经从高层到底层,探索了 LevelDB 的大部分核心组件:从内存中的 内存表 (MemTable),到磁盘上的 排序字符串表 (SSTable),再到管理它们的 Compaction 和 VersionSet。接下来,让我们把目光聚焦到构成 SSTable 的更小单元上。SSTable 文件内部的数据并不是杂乱无章地堆放的,而是被组织成一个个的"块"。

相关推荐
数智顾问3 小时前
实战:基于 BRPC+Etcd 打造轻量级 RPC 服务——从注册到调用的核心架构与基础实现
数据库
丶西红柿丶3 小时前
Mysql分区
数据库
檀越剑指大厂3 小时前
让数据触手可及采用Chat2DB+cpolar重构数据库操作体验
数据库·重构
朝九晚五ฺ3 小时前
【Redis学习】Redis中常见的全局命令、数据结构和内部编码
数据库·redis·学习
qq_508823404 小时前
金融数据库--下载全市场股票日线行情数据
数据库·金融
DemonAvenger4 小时前
MySQL性能优化案例分析:从问题到解决方案
数据库·mysql·性能优化
老华带你飞5 小时前
寝室快修|基于SprinBoot+vue的贵工程寝室快修小程序(源码+数据库+文档)
java·数据库·vue.js·spring boot·小程序·毕设·贵工程寝室快修