leveldb源码解析之三Get实现

导读

本篇博文主要是记录leveldb的Get实现!Get的流程从宏观上来说非常简单,无非是递归往下找,直到找到或者没有!历程为:

leveldb源码解析之三Get实现_第1张图片
image.png

如图,很直观,先在内存中的两个table查找,找不到就去sstable(level 0 ~ n,至于n是多少可以指定,一般是10)中找.。

step by step

让我们一步一步看Get的流程是如何的。

1. Get函数

首先我们从db_impl中的Get函数入手,函数原型为:

Status DBImpl::Get(const ReadOptions& options,
                   const Slice& key,
                   std::string* value) 
//status 是leveldb自己定义的用来处理各种状态返回的
//ReadOptions提供一些读取参数,目前可忽略
//key是要查找的key,而value是指针,用来保存查找到的值

很直观,其实核心就是根据key获取到value。让我们看看其实现:

Status DBImpl::Get(const ReadOptions& options,
                   const Slice& key,
                   std::string* value) {
  Status s;
  MutexLock l(&mutex_);
  SequenceNumber snapshot;
  //版本号,可以读取指定版本的数据,否则读取最新版本的数据.
  //注意:读取的时候数据也是会插入的,假如Get请求先到来,而Put后插入一条数据,这时候新数据并不会被读取到!
  if (options.snapshot != NULL) {
    snapshot = reinterpret_cast(options.snapshot)->number_;
  } else {
    snapshot = versions_->LastSequence();
  }

  //分别获取到memtable和Imuable memtable的指针
  MemTable* mem = mem_;
  MemTable* imm = imm_;
  Version* current = versions_->current();
  //增加reference计数,防止在读取的时候并发线程释放掉memtable的数据
  mem->Ref();
  if (imm != NULL) imm->Ref();
  current->Ref();

  bool have_stat_update = false;
  Version::GetStats stats;

  // Unlock while reading from files and memtables
  {
    mutex_.Unlock();
    // First look in the memtable, then in the immutable memtable (if any).
    //LookupKey是由key和版本号的封装.用来查找,不然每次都要传两个参数.把高耦合的参数合并成一个数据结构!
    LookupKey lkey(key, snapshot);
    if (mem->Get(lkey, value, &s)) { //memtable中查找
      // Done
    } else if (imm != NULL && imm->Get(lkey, value, &s)) { //Imuable memtable中查找
      // Done
    } else {
      s = current->Get(options, lkey, value, &stats);  //sstable中查找(内存中找不到就会进入这一步)
      have_stat_update = true;
    }
    mutex_.Lock();
  }

  if (have_stat_update && current->UpdateStats(stats)) {
    MaybeScheduleCompaction();  //检查是否要进行compaction操作
  }

  //释放引用计数. ps:自己维护一套这样的机制思维要非常清晰,否则很容易出bug.
  mem->Unref();
  if (imm != NULL) imm->Unref();
  current->Unref();
  return s;
}

对于函数内部的实现大部分都在代码中通过注释的形式来说明了:其实代码逻辑基本都是建立在一个大的框架中,然后一点一点填充内容而已。如果我们总结一下,我们可以知道上面代码的框架就是:

  1. 获取版本号,只读取该版本号之前的数据;
  2. 在memtable中查找
  3. 在Imuable memtable中查找
  4. 在sstable(磁盘文件)中查找
    所以我觉得看完这段代码,知道这四个步骤基本差不多了。

接下来我想我们会深入看看2,3,4这三个步骤的具体实现,尤其是4.因为我们知道其实2和3不过是在跳跃表中查找而已。这种内存数据结构的查找无疑大家应该是熟悉的,就跟在hashmap查找一样!

2. memtable和Imuable memtable查找

memtable和Imuable memtable在数据结构层面是一样的东西,也是一样的实现,只不过被使用的时候Imuable memtable加了只读的限制!

leveldb源码解析之三Get实现_第2张图片
image.png

简单不!就是通过迭代器在跳跃表中查找,找到后解码(由于数据被按照二进制格式封装起来了)构造结果返回。就是这么简单的两个步骤!

3. sstable查找

在sstable中的查找就比较复杂了,涉及到了许多文件的读取,我们一点一点剖析!
在进入复杂的逻辑之前,我们先掌握以下脉络是非常重要的。而sstable中的查找脉络就是一个for循环:

  for (int level = 0; level < config::maxLevel; level ++) {
        // seek
  }

简单吧,就是从level 0中的文件中开始查找,直到最大的level,如果中间找到就直接返回了。

我们这里还需特别强调一下,level 0的数据是Imuable memtable直接dump到磁盘的,所以文件与文件之间的key有可能重叠的。而level n(n>0)中每个sst文件之间key是不重叠的,且key在level中是全局有序的(注意是该level中)。

那么在每一层中是如何查找key的呢?答案很简单,不外乎两个步骤:

  1. 找到所有可能含有该key的文件列表fileList;
  2. 遍历fileList查找key;

第2步就是读取文件内容找出key而已,那么1是如何实现的呢?这里我们有必要复习一下前面的内容。我们除了sst文件(实际数据文件),leveldb还有manifest文件,该文件保存了每个sst文件在哪一层,最小key是啥,最大key是啥?所以:

我们通过读取manifest文件就能知道key有可能在哪一个sst文件中!

好了,大概的脉络到这里应该清楚了,我们来看代码:

Status Version::Get(const ReadOptions& options,
                    const LookupKey& k,
                    std::string* value,
                    GetStats* stats) {
  Slice ikey = k.internal_key();
  Slice user_key = k.user_key();
  const Comparator* ucmp = vset_->icmp_.user_comparator();
  Status s;

  stats->seek_file = NULL;
  stats->seek_file_level = -1;
  FileMetaData* last_file_read = NULL;
  int last_file_read_level = -1;

  // We can search level-by-level since entries never hop across
  // levels.  Therefore we are guaranteed that if we find data
  // in an smaller level, later levels are irrelevant.
  std::vector tmp;
  FileMetaData* tmp2;
  for (int level = 0; level < config::kNumLevels; level++) {
    
    /*-----------------找到可能包含key的文件列表begin------------------------*/
    size_t num_files = files_[level].size();
    if (num_files == 0) continue;

    // Get the list of files to search in this level
    FileMetaData* const* files = &files_[level][0];
    if (level == 0) { //level0特殊对待,key有可能在任何一个level0的文件中
      // Level-0 files may overlap each other.  Find all files that
      // overlap user_key and process them in order from newest to oldest.
      tmp.reserve(num_files);
      for (uint32_t i = 0; i < num_files; i++) {
        FileMetaData* f = files[i];
        if (ucmp->Compare(user_key, f->smallest.user_key()) >= 0 &&
            ucmp->Compare(user_key, f->largest.user_key()) <= 0) {
          tmp.push_back(f); //如果查找key落在该文件大小范围,则加到文件列表供下面进一步查询
        }
      }
      if (tmp.empty()) continue;

      std::sort(tmp.begin(), tmp.end(), NewestFirst);
      files = &tmp[0];
      num_files = tmp.size();
    } else {
      // Binary search to find earliest index whose largest key >= ikey.
      uint32_t index = FindFile(vset_->icmp_, files_[level], ikey); //直接找到在哪一个文件中,或者不在这个level
      if (index >= num_files) {
        files = NULL;
        num_files = 0;
      } else {
        tmp2 = files[index];
        if (ucmp->Compare(user_key, tmp2->smallest.user_key()) < 0) {
          // All of "tmp2" is past any data for user_key
          files = NULL;
          num_files = 0;
        } else {
          files = &tmp2;
          num_files = 1;
        }
      }
    }
    /*-----------------找到可能包含key的文件列表end------------------------*/


    /*-----------------遍历文件查找key begin------------------------*/
    for (uint32_t i = 0; i < num_files; ++i) {          //如果num_files不为0,说明key有可能在这些文件中
      if (last_file_read != NULL && stats->seek_file == NULL) {
        // We have had more than one seek for this read.  Charge the 1st file.
        stats->seek_file = last_file_read;
        stats->seek_file_level = last_file_read_level;
      }

      FileMetaData* f = files[i];
      last_file_read = f;
      last_file_read_level = level;

      Saver saver;
      saver.state = kNotFound;
      saver.ucmp = ucmp;
      saver.user_key = user_key;
      saver.value = value;
      s = vset_->table_cache_->Get(options, f->number, f->file_size,
                                   ikey, &saver, SaveValue); //在cache中读取文件的内容,至于cache实现,现在先不进入细节
      if (!s.ok()) {
        return s;
      }
      switch (saver.state) { //查找结果返回!
        case kNotFound:
          break;      // Keep searching in other files
        case kFound:
          return s;
        case kDeleted:
          s = Status::NotFound(Slice());  // Use empty error message for speed
          return s;
        case kCorrupt:
          s = Status::Corruption("corrupted key for ", user_key);
          return s;
      }
    }
    /*-----------------遍历文件查找key begin------------------------*/
    
  }

  return Status::NotFound(Slice());  // Use an empty error message for speed
}

代码很长,其实就是两部分。所以掌握脉络是多么重要!

其实上面已经差不多把Get的流程跑了一遍了,但是有一点特别有意思得还想在这里交代一下:我们在上面代码中发现在sst文件中查找的时候用到了cache,毕竟要读取磁盘。这里想深入进去看看这个cache是咋搞的?

cache 你好

LRU Cache

leveldb所用到的cache是LRUCache,这个大家学操作系统的时候应该都学过,这里不详细叙述了,简单说几句这个原理(使用java的linkedHashMap可以非常简单实现!

这里多说一句:在学生时代一直对这个概念有着错误的理解,当时觉得是什么鬼?如果大家结合java的linkedHashMap来思考应该是很简单的。

leveldb源码解析之三Get实现_第3张图片
image.png

如图,要注意这是一个linkedlist加上hashmap的性质。linkedlist的属性方便删除插入,hashmap的性质能在线性时间查找。这样队首元素就是最近最少使用的,可以被替换掉!

上面图中访问到的添加到队列前面可以在代码中清晰看到(cache.cc文件):


leveldb源码解析之三Get实现_第4张图片
image.png

先删除然后append到队尾!

Leveldb cache

我们先来看一下leveldb中是如何读取一个文件的。

1. 根据filenumber读取对应的数据文件:
leveldb源码解析之三Get实现_第5张图片
image.png

很简单两个步骤:

  1. cache中查找
  2. cache中找不到则直接磁盘中读取,并且插入cache
那么这里还有一个问题:在哪里淘汰?

答曰:在Insert函数内部。让我们来看看代码(cache.cc文件):

leveldb源码解析之三Get实现_第6张图片
image.png

上面我们只是讲解了leveldb中是如何运用LRUCache的。可是我们还没讲解cache中cache的数据是什么数据?是单个sst文件的数据?还是文件句柄数据?还是啥啥啥?
让我们继续深入去看看。

LRUHandle

我们知道队列元素是LRUHandle。而LRUHandle中的value就是我们实际缓存的数据:

leveldb源码解析之三Get实现_第7张图片
image.png

那么这个value的数据是在哪里添加进去的呢?(table_cache.cc):

leveldb源码解析之三Get实现_第8张图片
image.png

这个value指针实际是TableAndFile的指针。我们获取到一个LRUHandle之后就可以得到一个TableAndFile指针,里面包含了:

leveldb源码解析之三Get实现_第9张图片
image.png

RandomAccessFile是对读取文件的封装。因此我们可以读取想要的数据内容了。

总结

我们这篇文章主要讲解了数据是如何读取的以及cache是如何实现的。当然讲的还是脉络,很多细节都没涉及到。不过我相信有了脉络的掌握,再去阅读细节就非常简单的了。

你可能感兴趣的:(leveldb源码解析之三Get实现)