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

相关推荐
NiNg_1_23440 分钟前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠6 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#