LevelDB之Version

前言

在前面的文章中提到,MemTable 会转换为SSTable,SSTable 又分为多个层。Log中也会记录当前已经成功执行的指令。但是如何有效组织SSTable的层级,如何从崩溃状态恢复到崩溃前的状态呢?这里使用的组件就是Version。Version在LevelDB中的作用就是标识每一个文件的原始信息,如果SSTable 发生了变化,就会创建一个新的Version。

Version VersionEdit 和VersionSet

Version标识的是当前或者历史的某一时刻的文件,VersionEdit 表示是从上一个version到下一个version的中间态。VersionSet则是Version的集合,具体是一个双向链表。关系如下图:

每次文件发生变化都会创建出一个新的VersionEdit,最后变成新的Version,最新的Version是Version链表的尾部节点,会有一个current指向他。在LevelDB中,涉及文件变化的地方有

  1. MemTable 写入SSTable 包含了正常写入或者Recovery等
  2. 发生了SSTable的合并

每次变化都涉及到LevelDB的最重要的元数据变化,即文件的归属,MemTable转换为SSTable,所以在LevelDB中会将该操作持久化,即MANIFEST文件。但是如果当前DB一直运行,这个文件肯定会持续变大,最好的方法是定时清理,但是LevelDB并没有做这个操作,而是每次启动的时候会根据配置判断是否新建一个MANIFEST文件,而当前的MANIFEST的名称会被写在CURRENT文件,每次重启的时候会先读取current文件,然后获取里面的MANIFEST文件名称,从而读取里面的文件。因为此类操作其实和WAL日志差不多,所以读写都是复用的Log的写入,只是Record的内容不一样而已。

MANIFEST 文件的读写

在前面的MemTable中提到过,会将整个MemTable中的最大值,最小值记录到一个meta中,这个meta就是Version中的FileMetaData,里面的属性有:

arduino 复制代码
int refs; // 被version引用的值
int allowed_seeks;  // Seeks allowed until compaction 记录被查找的次数,用来做合并处理的
uint64_t number; // 文件号
uint64_t file_size;    // File size in bytes 当前文件的大小,BuildTable中的MemTable的大小
InternalKey smallest;  // Smallest internal key served by table // 当前文件最小的Key
InternalKey largest;   // Largest internal key served by table //当前文件最大的Key

在将MemTable 写入到SSTable的时候,首先就是获取当前的Version

ini 复制代码
  VersionEdit edit;
  Version* base = versions_->current();
  base->Ref();
  Status s = WriteLevel0Table(imm_, &edit, base);
  base->Unref();

Ref 方法是用来给Version做引用计数的,这个是用于析构函数的,的那个refs的值小于0 的时候就可以回收掉这个Version了。Unref 则是引用减一。

在调用WriteLevel0Table 之前,此时的edit为初始默认值,Base为current的Version。详细看下WriteLevel0Table的方法:

scss 复制代码
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
                                Version* base) {
  mutex_.AssertHeld(); // 判断是是否拥有锁
  const uint64_t start_micros = env_->NowMicros(); // 开始时间
  FileMetaData meta; // 新建Meta数据
  meta.number = versions_->NewFileNumber();// 从version_set中获取下一个文件号,这个文件号是全局递增的
  pending_outputs_.insert(meta.number); //用于记录使用的文件,避免合并过程中将正在使用的文件删除的情况
  Iterator* iter = mem->NewIterator();
  Log(options_.info_log, "Level-0 table #%llu: started",
      (unsigned long long)meta.number);
​
  Status s;
  {
    mutex_.Unlock(); // 这里释放了锁,也就是说当前可能存在后台线程在删除文件
      // 创建Table,生成文件,将file的部分原信息(最大最小key,size)
    s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
    mutex_.Lock(); // 创建完以后重新获取锁
  }
​
  Log(options_.info_log, "Level-0 table #%llu: %lld bytes %s",
      (unsigned long long)meta.number, (unsigned long long)meta.file_size,
      s.ToString().c_str());
  delete iter;
   // 从pending_outputs_释放,为什么这里可以释放呢,因为此时已经获取了mutex_锁,所以删除文件的方法就无法获取到这个锁从而删除数据了
  pending_outputs_.erase(meta.number);
​
  // Note that if file_size is zero, the file has been deleted and
  // should not be added to the manifest.
  int level = 0;
  if (s.ok() && meta.file_size > 0) {
    const Slice min_user_key = meta.smallest.user_key();
    const Slice max_user_key = meta.largest.user_key();
    if (base != nullptr) {
        // 尽可能的将文件往多的层级推
      level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
    }
      // 将文件写入到version set的newfiles中
    edit->AddFile(level, meta.number, meta.file_size, meta.smallest,
                  meta.largest);
  }
 
  CompactionStats stats;
  stats.micros = env_->NowMicros() - start_micros;
  stats.bytes_written = meta.file_size;
  stats_[level].Add(stats);
  return s;
}

此时Edit中包含了当前file的Meta和level

ini 复制代码
  void AddFile(int level, uint64_t file, uint64_t file_size,
               const InternalKey& smallest, const InternalKey& largest) {
    FileMetaData f;
    f.number = file;
    f.file_size = file_size;
    f.smallest = smallest;
    f.largest = largest;
    new_files_.push_back(std::make_pair(level, f));
  }

为什么这里是new_files new,按道理说此时的数据edit是new出来的,岂不是每次应该只有一个edit吗?其实单独从MemTable 写入SStable的方法CompactMemTable 方法来看,确实如此。但是在recovery的时候就不一定的,肯定存在两个MemTable 都可以Compact的情况,但是可以看做一个version,毕竟是在恢复状态。

接下来就进入到了Edit 到version的变化:

scss 复制代码
  // Replace immutable memtable with the generated Table
  if (s.ok()) {
    edit.SetPrevLogNumber(0);
    edit.SetLogNumber(logfile_number_);  // Earlier logs no longer needed
    s = versions_->LogAndApply(&edit, &mutex_);
  }

LogAndApply 方法中包含了version 的使用,一个就是从edit中新建Version

scss 复制代码
  Version* v = new Version(this);
  {
    Builder builder(this, current_);
    builder.Apply(edit);
    builder.SaveTo(v);
  }
  Finalize(v);

然后就是将Version 写入current中:

ini 复制代码
void VersionSet::AppendVersion(Version* v) {
  // Make "v" current
  assert(v->refs_ == 0);
  assert(v != current_);
  if (current_ != nullptr) {
    current_->Unref();
  }
  current_ = v;
  v->Ref();
​
  // Append to linked list
  v->prev_ = dummy_versions_.prev_;
  v->next_ = &dummy_versions_;
  v->prev_->next_ = v;
  v->next_->prev_ = v;
}

写入到MAINFEST中的文件格式是:

ini 复制代码
  kComparator = 1,
  kLogNumber = 2,
  kNextFileNumber = 3,
  kLastSequence = 4,
  kCompactPointer = 5,
  kDeletedFile = 6,
  kNewFile = 7,
  // 8 was used for large value refs
  kPrevLogNumber = 9

就是上面的Tag,加上对应的值。最后写入方式和Log的整体写入方式一致,解析方式其实也就是按照Tag将数据还原,这里就不赘述了。

总结

比较潦草的解释了下Version的创建和作用,Version 的主要作用还是为了组织当前的SSTable文件。

相关推荐
朝新_5 分钟前
【SpringBoot】详解Maven的操作与配置
java·spring boot·笔记·后端·spring·maven·javaee
绝无仅有12 分钟前
某教育大厂面试题解析:MySQL索引、Redis缓存、Dubbo负载均衡等
vue.js·后端·面试
sean23 分钟前
开发一个自己的 claude code
前端·后端·ai编程
追逐时光者1 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 59 期(2025年10.20-10.26)
后端·.net
盖世英雄酱581362 小时前
java深度调试【第三章内存分析和堆内存设置】
java·后端
007php0073 小时前
京东面试题解析:同步方法、线程池、Spring、Dubbo、消息队列、Redis等
开发语言·后端·百度·面试·职场和发展·架构·1024程序员节
程序定小飞3 小时前
基于springboot的电影评论网站系统设计与实现
java·spring boot·后端
码事漫谈3 小时前
高性能推理引擎的基石:C++与硬件加速的完美融合
后端
码事漫谈3 小时前
C++与边缘AI:在资源荒漠中部署智能的工程艺术
后端
绝无仅有3 小时前
腾讯面试文章解析:MySQL慢查询,存储引擎,事务,结构算法等总结与实战
后端·面试·github