LevelDB源码分析--Cache及Get查找流程

本打算接下来分析version相关的概念,但是在准备的过程中看到了VersionSet的table_cache_这个变量才想起还有这样一个模块尚未分析,经过权衡觉得leveldb的version相对Cache来说相对复杂,而且version虽然对整个leveldb来说实现上跟其他功能十分紧密,但是从概念上来说却相对弱很多,有点感觉是附加的功能的感觉。所以从介绍系统首先应该注意的是整个系统概念的完整性的角度说还是先分析Cache相关的功能。

我们先来看Cache的基本框架结构数据:

struct LRUHandle {
  void* value;      //cache的对象句柄,table的时候为table&file, block时为(table&file)_offset
  void (*deleter)(const Slice&, void* value);    //回调函数
  LRUHandle* next_hash;                          //hash表冲突解决指针
  LRUHandle* next;                               //LRU双链表指针
  LRUHandle* prev;                               //LRU双链表指针
  size_t charge;                                 // TODO(opt): Only allow uint32_t?
  size_t key_length;
  uint32_t refs;
  uint32_t hash;                                //根据key计算出的hash值,实现在hash.cc中
  char key_data[1];                             // encode后的file_num
};

HandleTable是一个简单的hashtable的链式实现,其成员如下:

HandleTable{
  uint32_t length_;
  uint32_t elems_;
  LRUHandle** list_;
};

LRUCache包含了一个HashTable(HandleTable)和一个双向链表头及容量、使用情况、互斥锁,每当insert的时候会同时插入HashTable中和双向链表中。

LRUCache{
  size_t capacity_;
  port::Mutex mutex_;
  size_t usage_;
  LRUHandle lru_;
  HandleTable table_;
};

LRUCache的与HandleTable和LRUHandle(途中绿色框)之间的基本关系图可以描述如下:

为了图形的简化,其中的地址和对象的关系未完整展现,lru_是对象而其他的绿色框都应该是表示的地址,大致图形便于理解,具体的关系请参阅源码。

ShardedLRUCache结构就更为简单,就是一个LRUCahce的数组,这样可以简单的集合封装多个LRUCache来达到分片降低锁的粒度增加并发度的目的。

在了解了基本关系之后要去理解其中的代码就十分简单了,这里不再一一列举,唯一要说明的是HandleTable 的hash自动增长模式,当HandleTable中的element 大于其hash数组大小的时候就对数组进行resize为最小的大于当前element个数的4的倍数,再将旧的element迁移到新的hash数组中。

if (elems_ > length_)
        Resize();
void Resize() {
    uint32_t new_length = 4;
    while (new_length < elems_) {
      new_length *= 2;
    }
    LRUHandle** new_list = new LRUHandle*[new_length]; //新建hash数组
    for (uint32_t i = 0; i < length_; i++) { //将原始的element迁移至新hash数组
      LRUHandle* h = list_[i];
      while (h != NULL) {
        LRUHandle* next = h->next_hash;
        uint32_t hash = h->hash;
        LRUHandle** ptr = &new_list[hash & (new_length - 1)];
        h->next_hash = *ptr;
        *ptr = h;
        h = next;
        count++;
      }
    }
    delete[] list_;//删除就数组、使用新表
    list_ = new_list;
    length_ = new_length;
  }
};

以上就是leveldb中关于LRUCache的基本结构了,但是leveldb在使用过程中又进行了一些变异和封装,如TableCache和DBImpl中的一个block_cache。我们这里先来对这两个概念进行一下梳理:由代码可以看出TableCache缓存的只是一个Table对象和RandomAccessFile对象的引用。而从Table::Open函数可以知道这个Table对象只保存了基本的管理信息(具体包括的内容前面的文章已经阐述过,请仔细查证),所以Table中的实际数据并未缓存其中。那么其中的实际数据在什么地方缓存呢?这里leveldb用到了另外的一个option中的ShardedLRUCache,当然这个cache由于是在option中那么说明其实可以改变的,你可以根据你的业务目标自行设计一个。到此你可能有点晕了,leveldb我要在cache中获取一个KV对的流程是怎么样的呢?这么设计的原因是什么呢?

第一个问题:leveldb中的Cache其实也是实现了分层,先cache一个SSTable的基本信息,而不是将整个SSTable的读到内存;再用一个cache缓存SSTable下一层级的实际的Block数据。那么获取数据的时候就得先根据基本信息获取到大致的SSTable,得到SSTable的句柄,再根据SSTable缓存中的基本信息获得应该去哪个block的信息,然后再根据block的句柄得到(SSTable句柄+Block句柄)去block缓存中获取实际的数据。

第二个问题:由于分层而不是将整个SSTable一次性的缓存到内存,那么得到的好久就显而易见了,可以减少内存的占用量。

我们来浏览一下Table_cache中各个函数的功能,首先看Get

Status TableCache::Get(const ReadOptions& options,
                       uint64_t file_number,       //file句柄
                       uint64_t file_size,
                       const Slice& k,                  //查找的key
                       void* arg,
                       void (*saver)(void*, const Slice&, const Slice&)) {
  Status s = FindTable(file_number, file_size, &handle); //查找到Cache handle
  if (s.ok()) {
    Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
    s = t->InternalGet(options, k, arg, saver);
    cache_->Release(handle);
  }
  return s;
}

再看FindTable:

Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
                             Cache::Handle** handle) {

  EncodeFixed64(buf, file_number); //根据file_num组一个key
  Slice key(buf, sizeof(buf));
  *handle = cache_->Lookup(key); //查找当前SSTable的信息是否已经在Table_cache中
  if (*handle == NULL) {          //如果不在则打开SSTable,
    std::string fname = TableFileName(dbname_, file_number);
    s = env_->NewRandomAccessFile(fname, &file);//尝试ldb后缀
    if (!s.ok()) {
      std::string old_fname = SSTTableFileName(dbname_, file_number); //尝试sst后缀
      if (env_->NewRandomAccessFile(old_fname, &file).ok()) {
        s = Status::OK();
      }
    }
    if (s.ok()) {
      s = Table::Open(*options_, file, file_size, &table); //读取文件管理信息生成Table对象
    }
    if (!s.ok()) {
    } else { //将内容缓存至TableCache中
      TableAndFile* tf = new TableAndFile;
      tf->file = file;
      tf->table = table;
      *handle = cache_->Insert(key, tf, 1, &DeleteEntry);
    }
  }
  return s;
}

再看Table的InternalGet,其功能是在SSTable中查找key

Status Table::InternalGet(const ReadOptions& options, const Slice& k,
                          void* arg,
                          void (*saver)(void*, const Slice&, const Slice&)) {
  Iterator* iiter = rep_->index_block->NewIterator(rep_->options.comparator);
  iiter->Seek(k); //查找key可能存在的block
  if (iiter->Valid()) {
    if (filter != NULL &&
        handle.DecodeFrom(&handle_value).ok() &&
        !filter->KeyMayMatch(handle.offset(), k)) { //根据bloomfilter判断是否在块中
      // Not found
    } else {
      Iterator* block_iter = BlockReader(this, options, iiter->value());
      block_iter->Seek(k); //读取block内容然后在block中查找
      if (block_iter->Valid()) {
        (*saver)(arg, block_iter->key(), block_iter->value());
      }
      s = block_iter->status();
      delete block_iter;
    }
  }
  if (s.ok()) {
    s = iiter->status();
  }
  delete iiter;
  return s;
}

另外TableCache中还有一个NewIterator的函数,顾名思义他就是生成一个遍历Cache的SSTable的迭代器,他基本上也是简单的调用Table 的 NewIterator。leveldb的Iterator设计也比较精妙,比如NewTwoLevelIterator这个东西,我们稍后会有篇幅来介绍,这里你只需要知道他就是生成一个遍历SSTable的迭代器就可以了。

理清了基本数据关系,最后就该轮到介绍我们大家最关心的DB_Impl的Get函数了,由于DB_Impl是DB虚类的一个子类,所以用户调用DB的get的时候实际调用的是这个函数的实现。

Status DBImpl::Get(const ReadOptions& options,
                   const Slice& key,
                   std::string* value) {
  // 锁,Sequence,version,ref等一系列的一些设置
  {
    //查找是否在memtable中,这是最新的数据
    if (mem->Get(lkey, value, &s)) {
      // Done
    } else if (imm != NULL && imm->Get(lkey, value, &s)) {
      // 查找是否在imutable memtable中,这是次新的数据
    } else {
      s = current->Get(options, lkey, value, &stats); //否则到当前版本中的SSTable中查找
      have_stat_update = true;
    }
  }
  //其他一些处理,以及判断是否需要出发compaction
  return s;
}

当前version中的查找:

Status Version::Get(const ReadOptions& options,
                    const LookupKey& k,
                    std::string* value,
                    GetStats* stats) {
  // 从0级开始一级一级查找,0级最新,1级次新,依次更旧,
  //所以查找的时候不能跳跃级别 ,找到最新的数据以后旧的数据就不在需要了
   for (int level = 0; level < config::kNumLevels; level++) {
    size_t num_files = files_[level].size();
    if (num_files == 0) continue;
    //取得文件句柄
    FileMetaData* const* files = &files_[level][0];
    if (level == 0) {
      // Level-0 需要查找全部文件,因为level-0中可能存在重叠
      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);
        }
      }
      //按文件新旧程度排序,新的在最前面
      std::sort(tmp.begin(), tmp.end(), NewestFirst);
      files = &tmp[0];
      num_files = tmp.size();
    } else {
      // 找到第一个 largest key >= ikey的文件(SSTable).
      uint32_t index = FindFile(vset_->icmp_, files_[level], ikey);

        tmp2 = files[index];
        if (ucmp->Compare(user_key, tmp2->smallest.user_key()) < 0) {
          //  不在该文件中
          files = NULL;
          num_files = 0;
        } else {
          files = &tmp2;
          num_files = 1;
        }
    }
     //找到具体的SSTable以后在该SSTable的缓存中进行具体查找
      s = vset_->table_cache_->Get(options, f->number, f->file_size,
                                   ikey, &saver, SaveValue);
      switch (saver.state) {
        case kNotFound:
          break; // 继续查找,知道找出的所有文件都查找完
        case kFound:
          return s;
        case kDeleted:
          s = Status::NotFound(Slice()); // 已经被删除,直接返回
          return s;
        case kCorrupt:
          s = Status::Corruption("corrupted key for ", user_key);
          return s;
      }
    }
  }
  return Status::NotFound(Slice()); // Use an empty error message for speed
}

LevelDB源码分析--Cache及Get查找流程

时间: 2024-10-12 16:34:18

LevelDB源码分析--Cache及Get查找流程的相关文章

leveldb源码分析--SSTable之Compaction

对于compaction是leveldb中体量最大的一部分,也应该是最为复杂的部分,为了便于理解我们首先从一些基本的概念开始.下面是一些从doc/impl.html中翻译和整理的内容: Level 0 当日志文件超过一定大小的阈值是 (默认为 1MB): 建立一个新的memtable和日志文件,以后的操作都是用新的memtable和日志文件 后台进行如下操作: 将旧的 memtable写到SSTable中(过程为先转为immtable_table,然后遍历写入) 废弃旧的 memtable 删除

leveldb源码分析--SSTable之逻辑结构

SSTable是leveldb 的核心模块,这也是其称为leveldb的原因,leveldb正是通过将数据分为不同level的数据分为对应的不同的数据文件存储到磁盘之中的.为了理解其机制,我们首先看看SSTable中的基本概念. 首先看看数据的整体存储结构: 可以从图中看到了几个概念:Datablock,Metablock, MetaIndex block, Indexblock, Footer.具体他们的含义可以大致解释如下: 1. Datablock,我们知道文件中的k/v对是有序存储的,他

leveldb源码分析--插入删除流程

由于网络上对leveldb的分析文章都比较丰富,一些基础概念和模型都介绍得比较多,所以本人就不再对这些概念以专门的篇幅进行介绍,本文主要以代码流程注释的方式. 首先我们从db的插入和删除开始以对整个体系有一个感性的认识,首先看插入: Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) { WriteBatch batch; //leveldb中不管单个插入还是多个插入都是以Wri

leveldb源码分析--Key结构

[注]本文参考了sparkliang的专栏的Leveldb源码分析--3并进行了一定的重组和排版 经过上一篇文章的分析我们队leveldb的插入流程有了一定的认识,而该文设计最多的又是Batch的概念.这篇文章本来应该顺理成章的介绍Batch相关流程和结构了,但是Batch涉及到了一些编码和Key相关的概念,所以我觉得应该先理清这方面的概念有助于大家更容易理解后面的内容. 在dbformat.h/cc文件中我们首先看到的是 typedef uint64_t SequenceNumber; str

leveldb源码分析--Comparator

既然leveldb是一个按Key序组织的LSM-Tree实现,那么对于Key的比较就是非常之重要了,这个Key的比较在leveldb中是Comparator的形式出现的.我们首先来看看Comparator的基本方法有哪些 // 实际的比较函数 virtual int Compare(const Slice& a, const Slice& b) const = 0; // 名称,主要是为了防止建立和读取时使用了不同的Comparator virtual const char* Name()

LevelDB源码分析--Iterator

我们先来参考来至使用Iterator简化代码2-TwoLevelIterator的例子,略微修改希望能帮助更加容易立即,如果有不理解请各位看客阅读原文. 下面我们再来看一个例子,我们为一个书店写程序,书店里有许多书Book,每个书架(BookShelf)上有多本书. 类结构如下所示 class Book { private: string book_name_; }; class Shelf { private: vector<Book> books_; }; 如何遍历书架上所有的书呢?一种实

leveldb源码分析--Memtable

本节讲述内存中LevelDB的数据结构Memtable,Memtable义如其名即为内存中的KV Table,即LSM-Tree中的C0 Tree.我们知道在LSM-Tree中刚插入的的KV数据都是存储在内存中,当内存中存储的数据超过一定量以后再写到磁盘中.而对于leveldb来说这个过程演变为内存中的数据都是插入到MemTable中,当MemTable中的数据超过一定量(Options.write_buffer_size)以后MemTable就转化为Immutable Memtable等待du

leveldb源码分析--SSTable之TableBuilder

上一篇文章讲述了SSTable的格式以后,本文结合源码解析SSTable是如何生成的. void TableBuilder::Add(const Slice& key, const Slice& value) { //如果已经插入过数据,那么要保证当前插入的key > 之前最后一次插入的key, // SSTable必须是有序的插入数据 if (r->num_entries > 0) { assert(r->options.comparator->Compar

leveldb源码分析--BloomFilter

bloomfilter是leveldb中的一大性能利器,所以为了文章的表现完整性这里新启这么一篇文章.leveldb中的bloomfilter的实现在bloom.cc中,是一个较为简单的实现,所以就不再具体进行分析.本文列出两个参考地址: 那些优雅的数据结构(1) : BloomFilter——大规模数据处理利器 是一个简单的bloomfilter的介绍乐和实现 Bloom Filter  则是一位专业的bloomfilter的研究人士的博客,内容及其的牛逼,对此感兴趣的阅读以下其文章应该有不小