levelDB为什么需要版本控制

在一个使用levelDB的服务中,必然存在多个线程同时访问数据库的情况。例如,如果正好有thread2正在访问sstable4。与此同时,thread1在写数据时,发生了compaction,level0中的sstable1需要与level1中的sstable4进行compaction,这是thread1应该基于当前version,去产出一个新的version,以免线程之间的互相影响,保证数据的一致性。

level0: sstable1 sstable2
level1: sstable3 sstable4 sstable5

FileMetaDatas类

FileMetaDatas是对每个本次磁盘文件sstable的描述,是磁盘文件和version的桥梁。而 version是词表文件和数据之间的桥梁。
之前我们在LevelDB-总体介绍 中提到一个疑问,levelDB是将磁盘文件以层的结构存在,那么哪里维护这个层结构呢,其实就是在Version类中。

FileMetaDatas 中记录着文件的名字,文件所占的字节大小,文件的最小InternalKey和最大的InternalKey以及有多少线程正在使用该文件。

版本控制

levelDB中,版本控制涉及的类有Version 、 VersionSet 、VersionEdit 以及 Build,他们之间的关系如下:

VersionSet 中维护一个双向链表,其中每一个节点为Version对象。在version类中,维护着一系列指向FileMetaData的指针。levelDB中任何对磁盘sstable的修改/增加/删除,首先将变更生成一个 VersionEdit 对象,然后基于Build类,生成一个新的Version,存储到VersionSet维护的双向链表中。
即 Version + VersionEdit = New Version,其中 += 操作由Build类完成。

首先明确一点:什么时候会发生版本变更:

就是在发生compaction的时候,在levelDB中compaction的类型有:

  1. minor compaction : immutable 到 sstable,不一定一定会落盘到level0层;
  2. size compaction :
  3. seek compaction:

本文着重讲解一下minor compaction。具体如何实现呢,下面简化LevelDB中的源码,大致讲一下这个过程:

// 首先初始化一个文件元信息
FileMetaData meta;
// memtbale的迭代器
Iterator* iter = mem->NewIterator();
// 真正地向磁盘写一个sstable,这时候,并不知道该sstable应该放到第几层
s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
// base 指向当前version
Version* base = ....;
// 判断刚才写入的sstable应该放到第几层中
level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
// 将SSTable文件序列号对应的层,Key等信息写入manifest中
edit->AddFile(level, meta.number, meta.file_size, meta.smallest,meta.largest);
Version* v = new Version(this);{Builder builder(this, current_);builder.Apply(edit);builder.SaveTo(v);}

VersionEdit 类

minor compaction的第一步就是将immutable转换成VersionEdit。
VersionEdit类是保存变更的类。其中有两个特别重要的类成员deleted_files_, new_files_。

new_files_ 记录新增sstable磁盘文件,采用pair结构记录,第一个参数记录的level,即放在第几层中,第二个记录的文件的元信息;
deleted_files_ 记录的是删除 第几层的那个sstable。

  typedef std::set<std::pair<int, uint64_t>> DeletedFileSet;// 和VersionSet里面的compact_pointers_相同std::vector<std::pair<int, InternalKey>> compact_pointers_;// 有哪些文件被删除,就是Version里哪些SSTable被删除DeletedFileSet deleted_files_;// 有哪些文件被增加,pair的第一个参数是Level,第二个参数是文件的元信息std::vector<std::pair<int, FileMetaData>> new_files_;

Version类

Version其实很好理解,就是记录着当前版本有那些文件,并且记录这些文件的层级结构。

class Version {public:
... Status Get(const ReadOptions&, const LookupKey& key, std::string* val,GetStats* stats);bool UpdateStats(const GetStats& stats);bool RecordReadSample(Slice key);// Reference count management (so Versions do not disappear out from// under live iterators)void Ref();void Unref();
... ... // Return the level at which we should place a new memtable compaction// result that covers the range [smallest_user_key,largest_user_key]./* 该函数用来选择  需要将从MemTable dump出的sstable file放入第几层*/int PickLevelForMemTableOutput(const Slice& smallest_user_key,const Slice& largest_user_key);/*判断某层level的文件个数*/int NumFiles(int level) const { return files_[level].size(); }// Return a human readable string that describes this version's contents.std::string DebugString() const;private:friend class Compaction;friend class VersionSet;class LevelFileNumIterator;explicit Version(VersionSet* vset): vset_(vset),next_(this),prev_(this),refs_(0),file_to_compact_(nullptr),file_to_compact_level_(-1),compaction_score_(-1),compaction_level_(-1) {}Version(const Version&) = delete;Version& operator=(const Version&) = delete;~Version();Iterator* NewConcatenatingIterator(const ReadOptions&, int level) const;void ForEachOverlapping(Slice user_key, Slice internal_key, void* arg,bool (*func)(void*, int, FileMetaData*));/*所有的version都属于一个集合即Version Set*/VersionSet* vset_;  // VersionSet to which this Version belongsVersion* next_;     // Next version in linked listVersion* prev_;     // Previous version in linked listint refs_;          // Number of live refs to this version// SSTable的信息,每一项代表相应Level的SSTable信息// 除了Level 0外,每个Level里的文件都是按照最小键的顺序排列的,并且没有重叠// 通过这个数据项,搜索SSTable时,就可以从Level 0开始搜索std::vector<FileMetaData*> files_[config::kNumLevels];   // Next file to compact based on seek stats.FileMetaData* file_to_compact_;   // 哪个文件需要 seek compactionint file_to_compact_level_;       // seek compaction 发生在哪一层中double compaction_score_;         // 所有level层中 最大的compaction coreint compaction_level_;           // size compaction  发生在哪一层中
};
/**
判断以smallest_user_key为最小值,larget_user_key为最大只的sstable应该放到当前version的第几层
**/
int Version::PickLevelForMemTableOutput(const Slice& smallest_user_key,const Slice& largest_user_key) {int level = 0;if (!OverlapInLevel(0, &smallest_user_key, &largest_user_key)) {// Push to next level if there is no overlap in next level,// and the #bytes overlapping in the level after that are limited.InternalKey start(smallest_user_key, kMaxSequenceNumber, kValueTypeForSeek);InternalKey limit(largest_user_key, 0, static_cast<ValueType>(0));std::vector<FileMetaData*> overlaps;while (level < config::kMaxMemCompactLevel) {if (OverlapInLevel(level + 1, &smallest_user_key, &largest_user_key)) {break;}if (level + 2 < config::kNumLevels) {// Check that file does not overlap too many grandparent bytes.GetOverlappingInputs(level + 2, &start, &limit, &overlaps);const int64_t sum = TotalFileSize(overlaps);if (sum > MaxGrandParentOverlapBytes(vset_->options_)) {break;}}level++;}}return level;
}

疑问:
为什么在Version类中,将VersionSet设置为友元,设置一个成员变量VersionSet* vset_?
我的理解: 所有一系列的Version构成了VersionSet,那么一个Version归属于那个VersionSet呢?就用这个成员对象进行记录。并且在Version的成员函数中需要使用VersionSet的私有变量,所以将VersionSet设置为友元。

下面重点讲解一下 Version::PickLevelForMemTableOutput 函数。在 immutable落盘sstable时,即 minor compaction 时使用。作用是判断新增sstable需要放在哪一层中。该函数的流程图如下所示:

首先读者要知道两点:

  1. 在levelDB中,level0的数据要比level1中的数据新,level1中的数据 要比level2中的数据新;
  2. level0中的sstable之间,key是可以有交集的,level1 … leveln中的sstable之间key之间是没有交集的。

回到 PickLevelForMemTableOutput 函数,下面详细说一下具体流程:

  1. 首先判断新增sstable是否与level0有交集,如果有交集,直接插入到level0中。
  2. 如果新增sstable与level1 没有交集,并且与level2中有交集的sstable个数小于阈值,则可以插入到level1中;
  3. 如果新增sstable与level2 没有交集,并且与level3中有交集的sstable个数小于阈值,则插入到level2中;
    从流程图中可以看出来,新增sstable最多就插入到level2中;

提问1:拿level1来举例子,为什么不光要判断是否与level1有交集,还要判断与level2的交集文件个数呢?
答案:首先对于>level1的层,层中的sstable之间是无交集的。所以第一个判断很好理解;第二个判断是问了降低size compaction的成本;假设new sstable插入到level1中,但与level2中的交集有10个(很多了)。当new sstable需要进行size compaction时,那么compaction的文件就可能达到>15个,那么本次size compaction的成本就相当高了。

VersionSet类

到这里,我们已经基于immutable生成了VersionEdit对象,并生成了 new version。那么现在,我们需要将new version应用起来。即让levelDB感知到新增的version。
这里只介绍添加VersionEdit对象的函数LogAndApply。 VersionSet类的具体细节后面再加,因为现在我也不太清楚,嘻嘻嘻

Status VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu) {if (edit->has_log_number_) {assert(edit->log_number_ >= log_number_);assert(edit->log_number_ < next_file_number_);} else {edit->SetLogNumber(log_number_);}if (!edit->has_prev_log_number_) {edit->SetPrevLogNumber(prev_log_number_);}edit->SetNextFile(next_file_number_);edit->SetLastSequence(last_sequence_);// 以上内容 我也没太明白Version* v = new Version(this);{Builder builder(this, current_);builder.Apply(edit);builder.SaveTo(v);}// 计算版本 v 中,compaction score // 到这里,新的version已经构造完毕Finalize(v);// Initialize new descriptor log file if necessary by creating// a temporary file that contains a snapshot of the current version.std::string new_manifest_file;Status s;if (descriptor_log_ == nullptr) {// No reason to unlock *mu here since we only hit this path in the// first call to LogAndApply (when opening the database).assert(descriptor_file_ == nullptr);new_manifest_file = DescriptorFileName(dbname_, manifest_file_number_);edit->SetNextFile(next_file_number_);s = env_->NewWritableFile(new_manifest_file, &descriptor_file_);if (s.ok()) {descriptor_log_ = new log::Writer(descriptor_file_);s = WriteSnapshot(descriptor_log_);}}// Unlock during expensive MANIFEST log write   // 日志操作{... }// Install the new versionif (s.ok()) {AppendVersion(v);log_number_ = edit->log_number_;prev_log_number_ = edit->prev_log_number_;} else {delete v;if (!new_manifest_file.empty()) {delete descriptor_log_;delete descriptor_file_;descriptor_log_ = nullptr;descriptor_file_ = nullptr;env_->RemoveFile(new_manifest_file);}}return s;
}// A helper class so we can efficiently apply a whole sequence
// of edits to a particular state without creating intermediate
// Versions that contain full copies of the intermediate state.
class VersionSet::Builder {private:// Helper to sort by v->files_[file_number].smalleststruct BySmallestKey {const InternalKeyComparator* internal_comparator;bool operator()(FileMetaData* f1, FileMetaData* f2) const {int r = internal_comparator->Compare(f1->smallest, f2->smallest);if (r != 0) {return (r < 0);} else {// Break ties by file numberreturn (f1->number < f2->number);}}};typedef std::set<FileMetaData*, BySmallestKey> FileSet;struct LevelState {std::set<uint64_t> deleted_files;FileSet* added_files;};VersionSet* vset_;Version* base_;LevelState levels_[config::kNumLevels];public:// Initialize a builder with the files from *base and other info from *vset// 根据VersionSet,构造internal的比较函数;// 构造磁盘level结构Builder(VersionSet* vset, Version* base) : vset_(vset), base_(base) {base_->Ref();BySmallestKey cmp;cmp.internal_comparator = &vset_->icmp_;for (int level = 0; level < config::kNumLevels; level++) {levels_[level].added_files = new FileSet(cmp);}}~Builder() {for (int level = 0; level < config::kNumLevels; level++) {const FileSet* added = levels_[level].added_files;std::vector<FileMetaData*> to_unref;to_unref.reserve(added->size());for (FileSet::const_iterator it = added->begin(); it != added->end();++it) {to_unref.push_back(*it);}delete added;for (uint32_t i = 0; i < to_unref.size(); i++) {FileMetaData* f = to_unref[i];f->refs--;if (f->refs <= 0) {delete f;}}}base_->Unref();}// Apply all of the edits in *edit to the current state.// 应用VersionEdit对象中的所有记录,填充Builder构造函数中 构造出来的层结构void Apply(VersionEdit* edit) {// Update compaction pointers// 这块不知道干嘛用的,需要再看 TODOfor (size_t i = 0; i < edit->compact_pointers_.size(); i++) {const int level = edit->compact_pointers_[i].first;vset_->compact_pointer_[level] =edit->compact_pointers_[i].second.Encode().ToString();}// Delete filesfor (const auto& deleted_file_set_kvp : edit->deleted_files_) {const int level = deleted_file_set_kvp.first;const uint64_t number = deleted_file_set_kvp.second;levels_[level].deleted_files.insert(number);}// Add new filesfor (size_t i = 0; i < edit->new_files_.size(); i++) {const int level = edit->new_files_[i].first;FileMetaData* f = new FileMetaData(edit->new_files_[i].second);f->refs = 1;// We arrange to automatically compact this file after// a certain number of seeks.  Let's assume://   (1) One seek costs 10ms//   (2) Writing or reading 1MB costs 10ms (100MB/s)//   (3) A compaction of 1MB does 25MB of IO://         1MB read from this level//         10-12MB read from next level (boundaries may be misaligned)//         10-12MB written to next level// This implies that 25 seeks cost the same as the compaction// of 1MB of data.  I.e., one seek costs approximately the// same as the compaction of 40KB of data.  We are a little// conservative and allow approximately one seek for every 16KB// of data before triggering a compaction.f->allowed_seeks = static_cast<int>((f->file_size / 16384U));if (f->allowed_seeks < 100) f->allowed_seeks = 100;levels_[level].deleted_files.erase(f->number);levels_[level].added_files->insert(f);}}// Save the current state in *v.void SaveTo(Version* v) {BySmallestKey cmp;cmp.internal_comparator = &vset_->icmp_;for (int level = 0; level < config::kNumLevels; level++) {// Merge the set of added files with the set of pre-existing files.// Drop any deleted files.  Store the result in *v.const std::vector<FileMetaData*>& base_files = base_->files_[level];std::vector<FileMetaData*>::const_iterator base_iter = base_files.begin();std::vector<FileMetaData*>::const_iterator base_end = base_files.end();const FileSet* added_files = levels_[level].added_files;v->files_[level].reserve(base_files.size() + added_files->size());for (const auto& added_file : *added_files) {// Add all smaller files listed in base_for (std::vector<FileMetaData*>::const_iterator bpos =std::upper_bound(base_iter, base_end, added_file, cmp);base_iter != bpos; ++base_iter) {MaybeAddFile(v, level, *base_iter);}MaybeAddFile(v, level, added_file);}// Add remaining base filesfor (; base_iter != base_end; ++base_iter) {MaybeAddFile(v, level, *base_iter);}#ifndef NDEBUG// Make sure there is no overlap in levels > 0if (level > 0) {for (uint32_t i = 1; i < v->files_[level].size(); i++) {const InternalKey& prev_end = v->files_[level][i - 1]->largest;const InternalKey& this_begin = v->files_[level][i]->smallest;if (vset_->icmp_.Compare(prev_end, this_begin) >= 0) {std::fprintf(stderr, "overlapping ranges in same level %s vs. %s\n",prev_end.DebugString().c_str(),this_begin.DebugString().c_str());std::abort();}}}
#endif}}void MaybeAddFile(Version* v, int level, FileMetaData* f) {if (levels_[level].deleted_files.count(f->number) > 0) {// File is deleted: do nothing} else {std::vector<FileMetaData*>* files = &v->files_[level];if (level > 0 && !files->empty()) {// Must not overlapassert(vset_->icmp_.Compare((*files)[files->size() - 1]->largest,f->smallest) < 0);}f->refs++;files->push_back(f);}}
};

提问的时间到了。
问题1: 为什么Builder类要设计在VersionSet类内?
答案:Builder是在VersionSet的private中声明的,Builder就作用就是将一些函数再次封装,使得整体代码,看起来更加整洁。这些函数只会在VersionSet类函数中使用。

levelDB 的版本控制相关推荐

  1. LevelDB原理及应用

    LevelDB LevelDB之概览 LevelDB是Google传奇工程师Jeff Dean和Sanjay Ghemawat开源的KV存储引擎.  了解原理之前首先要用起来,下面动手实现个例子:安装 ...

  2. 半小时学会LevelDB原理及应用

    LevelDB LevelDB之概览 LevelDB是Google传奇工程师Jeff Dean和Sanjay Ghemawat开源的KV存储引擎. 了解原理之前首先要用起来,下面动手实现个例子:安装调 ...

  3. Leveldb源码分析--15

    9 LevelDB框架之2 9.4 版本控制 当执行一次compaction后,Leveldb将在当前版本基础上创建一个新版本,当前版本就变成了历史版本.还有,如果你创建了一个Iterator,那么该 ...

  4. LevelDB库简介

    LevelDB库简介 一.LevelDB入门 LevelDB是Google开源的持久化KV单机数据库,具有很高的随机写,顺序读/写性能,但是随机读的性能很一般,也就是说,LevelDB很适合应用在查询 ...

  5. leveldb 原理解析

    目录 概览 Features 整体结构 Memtable Immutable Memtable SSTable 文件(SST) SSTable的物理结构 Block 物理结构 节省 key 占用空间 ...

  6. levelDB学习记录

    文章目录 levelDB学习记录 博客 LevelDB源码分析 1.起步 2.Visual Studio Code 3.基本思路 LevelDB四个接口 磁盘的特点 状态机 状态机的四个概念 初步设计 ...

  7. 从JoinBatchGroup 代码细节 来看Rocksdb的相比于leveldb的写入优势

    文章目录 1. Rocksdb写入模型 2. LevelDB写入的优化点 3. Rocksdb 的优化 1. Busy Loop 2. Short Wait -- SOMETIMES busy Loo ...

  8. 软件构造 第二章 第一节 软件生命周期和版本控制

    软件构造第二章 第一节 软件生命周期和版本控制 基本内容 Software Development Lifecycle (SDLC) Traditional software process mode ...

  9. 数据集cifar10到Caffe支持的lmdb/leveldb转换的实现

    在 http://blog.csdn.net/fengbingchun/article/details/53560637 对数据集cifar10进行过介绍,它是一个普通的物体识别数据集.为了使用Caf ...

最新文章

  1. 谷歌被指骗取人脸数据:部分获取方法可疑
  2. ABAP中的F4帮助怎么用?
  3. 实验二 二叉树的操作与实现
  4. Java关键字—instanceof
  5. Java字符编码介绍
  6. java自学路线图_JAVA自学路线图
  7. IPC--进程间通信四(信号量)
  8. python有趣小程序-第一个有趣的python小程序
  9. android 输入法出现挤压屏幕、android输入键盘覆盖了屏幕控件的解决办法
  10. 上传本地项目到gitee_使用git将本地代码上传到gitee远程仓库
  11. [C#] DBNull、Null和String.Empty的区别
  12. u盘pe安装深度linux系统教程,深度U盘装机大师_深度u盘启动盘安装深度系统
  13. android srgb模式,一加3固件官方更新:加入屏幕边缘防误触和sRGB显示模式功能
  14. 预测:深度学习未来的6种可能
  15. css3弹性盒子居中总结1
  16. 流数据分析之地理围栏应用
  17. 新机如何把机械硬盘中的系统克隆到固态硬盘
  18. 分布式调度问题及解决方案
  19. 知物由学|游戏开发者如何从容应对Unity手游风险?
  20. 电子邮箱怎么注册,公司用什么电子邮箱好

热门文章

  1. 寻找第n个默尼森数。
  2. 一次难忘的bug处理经历
  3. 为什么我的话总没说服力
  4. SDNU_ACM_ICPC_2020_Winter_Practice_4th(补题2020.2.2)
  5. python最小二乘法拟合直线_Python 实现最小二乘法拟合直线
  6. 耐威迪参加第十一届中国优秀数据中心峰会并做主题演讲
  7. MySQL批量更新数据总结
  8. Android拖拽排序控件DragGridView
  9. 网络安全学习笔记——第五天 防火墙功能特性
  10. 产品发布 | 京东云推出【云搜索】服务,海量数据全文检索从未如此简单