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是如何實現的。當然講的還是脈絡,很多細節都沒涉及到。不過我相信有了脈絡的掌握,再去閱讀細節就非常簡單的了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容