leveldb源码解析之三Get实现

导读

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

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<const SnapshotImpl*>(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加了只读的限制!

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<FileMetaData*> 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来思考应该是很简单的。

image.png

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

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


image.png

先删除然后append到队尾!

Leveldb cache

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

1. 根据filenumber读取对应的数据文件:
image.png

很简单两个步骤:

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

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

image.png

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

LRUHandle

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

image.png

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

image.png

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

image.png

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

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容