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文件。

相关推荐
罗政13 分钟前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师2 小时前
spring获取当前request
java·后端·spring
Java小白笔记3 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
JOJO___5 小时前
Spring IoC 配置类 总结
java·后端·spring·java-ee
白总Server6 小时前
MySQL在大数据场景应用
大数据·开发语言·数据库·后端·mysql·golang·php
Lingbug7 小时前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore
计算机学姐7 小时前
基于SpringBoot+Vue的篮球馆会员信息管理系统
java·vue.js·spring boot·后端·mysql·spring·mybatis
好兄弟给我起把狙7 小时前
[Golang] Select
开发语言·后端·golang
程序员大金7 小时前
基于SpringBoot+Vue+MySQL的智能物流管理系统
java·javascript·vue.js·spring boot·后端·mysql·mybatis
ac-er88889 小时前
在Flask中处理后台任务
后端·python·flask