leveldb深度剖析-TableCache

TableCache設計的出發點就是:提升性能。根據著名的局部性訪問原理,leveldb設計了一個簡單LRUCache算法,該算法是TableCache的核心,下面我們就來分析一下leveldb是如何實現的。

一、TableCache

先來看一下TableCache的類定義,非常簡潔:

class TableCache {
 public:
  TableCache(const std::string& dbname, const Options* options, int entries);
  ~TableCache();
  //迭代器
  Iterator* NewIterator(const ReadOptions& options,
                        uint64_t file_number,
                        uint64_t file_size,
                        Table** tableptr = NULL);
  // 查詢接口
  Status Get(const ReadOptions& options,
             uint64_t file_number,
             uint64_t file_size,
             const Slice& k,
             void* arg,
             void (*handle_result)(void*, const Slice&, const Slice&));

  // Evict any entry for the specified file number
  // 根據文件編號 從cache中刪除指定項
  void Evict(uint64_t file_number);

 private:
  Env* const env_;
  const std::string dbname_;
  const Options* options_;
  Cache* cache_; ////封裝LRUCache 在構造函數中初始化
  //根據文件編號 查找文件
  Status FindTable(uint64_t file_number, uint64_t file_size, Cache::Handle**);
};

1.1、查詢接口

/**
 * 從ldb中獲取數據
 * @param options 選項
 * @param file_number ldb文件編號  來自FileMetaData
 * @param file_size   ldb文件大小  來自FileMetaData
 * @param k           查找key值
 * @param arg         回調函數參數
 * @param saver       回調函數
 */
Status TableCache::Get(const ReadOptions& options,
                       uint64_t file_number,
                       uint64_t file_size,
                       const Slice& k,
                       void* arg,
                       void (*saver)(void*, const Slice&, const Slice&)) {
  Cache::Handle* handle = NULL;
  Status s = FindTable(file_number, file_size, &handle);// 打開ldb文件並且讀取出data index block以及meta index block
  if (s.ok()) {
    Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
    s = t->InternalGet(options, k, arg, saver);// 在table中查找k 如果找到則通過saver回調函數進行保存
    cache_->Release(handle);//必須調用
  }
  return s;
}

這裏強調一下:必須調用cache_->Release(handle)接口,查詢接口十分的簡單,代碼註釋也很清楚,下面來看一下FindTable具體實現 。

1.2、FindTable 

/**
 * 查找Table
 * @param file_number ldb文件編號  來自FileMetaData
 * @param file_size   ldb文件大小  來自FileMetaData
 * @param handle      cache handle對象 輸出參數
 */
Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
                             Cache::Handle** handle) {
  Status s;
  char buf[sizeof(file_number)];
  EncodeFixed64(buf, file_number);
  Slice key(buf, sizeof(buf));
  *handle = cache_->Lookup(key);//按照key進行查找 如果沒有找到則說明是新文件
  if (*handle == NULL) {
    std::string fname = TableFileName(dbname_, file_number);//讀取ldb文件
    RandomAccessFile* file = NULL;
    Table* table = NULL;
    s = env_->NewRandomAccessFile(fname, &file);
    if (!s.ok()) {// 兼容 1.13版本(含)之前是sst文件作爲後綴
      std::string old_fname = SSTTableFileName(dbname_, file_number);
      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()) {
      assert(table == NULL);
      delete file;
      // We do not cache error results so that if the error is transient,
      // or somebody repairs the file, we recover automatically.
    } else {
      TableAndFile* tf = new TableAndFile;//對文件對象和Table對象進行包裝
      tf->file = file;
      tf->table = table;//保存table對象
      
      //插入cache中 默認返回LRUHandle對象 此處key是文件編號
      *handle = cache_->Insert(key, tf, 1, &DeleteEntry);
    }
  }
  return s;
}

說明:

1)Cache 緩存的實際是ldb文件對象以及Table,可以通過Cache::Insert看出來。那麼查詢關鍵字是什麼呢?是文件編號,也就是文件名字中的數字。

2)如果通過cache->Lookup沒有找到說明是新文件,那麼就要執行open操作,對ldb文件進行解析,這個解析過程是無法避免的而且是比較耗時的,這也就是爲什麼leveldb設計了一個LRUCache的原因(根據局部性原理)。

3)上面Table::Open實際是讀取ldb文件並對其解析,例如解析出foot,index block,data block等,這部分是最好費性能的。

4)將file和table這兩個對象封裝到TableAndFile對象中並且將其插入到cache中。

二、ShardedLRUCache

類Cache只是接口,真正實現類爲ShardedLRUCache,這部分內容比較凌亂,涉及到一些類,我對其進行了總結:

類名稱 說明 備註
類Cache 抽象類,幾乎所有接口都是虛函數  
類ShardedLRUCache 繼承Cache,實現類  

類LRUCache

ShardedLRUCache定義了16分片,每個分片類型爲LRUCache 每個LRU是獨立的,線程安全的,可以提升併發訪問
結構體Handle 空結構體,用於接口。內部實際爲LRUHandle  
結構體LRUHandle 每個LRUHandle相當於cache中一個元素 cache組織方式是雙向鏈表,LRUHandle相當於鏈表中一個元素
類HandleTabble 爲了提升性能,leveldb還增加了HashTable用於存儲LRUHandle  

由於SharedLRUCache方法基本上是對LRUCache方法的封裝,所以這裏只羅列兩個方法,後面在介紹LRUCache的時候在詳細介紹。

static const int kNumShardBits = 4;
static const int kNumShards = 1 << kNumShardBits;//16  
explicit ShardedLRUCache(size_t capacity)
    : last_id_(0) {
  const size_t per_shard = (capacity + (kNumShards - 1)) / kNumShards;//每個分片最多緩存的個數
  for (int s = 0; s < kNumShards; s++) {
    shard_[s].SetCapacity(per_shard);//每個分片cache的容量爲per_shard,超過這個就需要移除舊元素
  }
}
  virtual Handle* Insert(const Slice& key, void* value, size_t charge,
                         void (*deleter)(const Slice& key, void* value)) {
    const uint32_t hash = HashSlice(key);//對key進行hash 然後插入到對應的cache中
    return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
  }

三、結構體LRUHandle

該結構體比較簡單,註釋已經給出詳細說明。

struct LRUHandle {
  void* value; //value
  void (*deleter)(const Slice&, void* value);
  LRUHandle* next_hash;// 在hashtable中使用 

  LRUHandle* next; //雙向鏈表
  LRUHandle* prev; //雙向鏈表

  size_t charge;      // 佔用cache空間數目,目前始終爲1
  size_t key_length;
  bool in_cache;      // 表示當前元素是否在cache中 false表示回收內存.
  uint32_t refs;      // 當引用計數爲0時就要刪除 
  uint32_t hash;      // Hash of key(); used for fast sharding and comparisons
  char key_data[1];   // key值

  Slice key() const {
    // For cheaper lookups, we allow a temporary Handle object
    // to store a pointer to a key in "value".
    if (next == this) {
      return *(reinterpret_cast<Slice*>(value));
    } else {
      return Slice(key_data, key_length);
    }
  }
};

四、LRUCache

下面來看一下LRUCache定義以及相關內容介紹。

// A single shard of sharded cache.
class LRUCache {
 public:
  LRUCache();
  ~LRUCache();

  // Separate from constructor so caller can easily make an array of LRUCache
  // 默認值是62
  void SetCapacity(size_t capacity) { capacity_ = capacity; }

  // Like Cache methods, but with an extra "hash" parameter.
  Cache::Handle* Insert(const Slice& key, uint32_t hash,
                        void* value, size_t charge,
                        void (*deleter)(const Slice& key, void* value));
  Cache::Handle* Lookup(const Slice& key, uint32_t hash);
  void Release(Cache::Handle* handle);
  void Erase(const Slice& key, uint32_t hash);
  void Prune();
  size_t TotalCharge() const {
    MutexLock l(&mutex_);
    return usage_;
  }

 private:
  void LRU_Remove(LRUHandle* e);
  void LRU_Append(LRUHandle*list, LRUHandle* e);
  void Ref(LRUHandle* e);
  void Unref(LRUHandle* e);
  bool FinishErase(LRUHandle* e);

  // Initialized before use. 默認值是62
  size_t capacity_; //容量 超過這個容量後就要移除舊數據

  // mutex_ protects the following state.
  mutable port::Mutex mutex_;
  size_t usage_; //當前cache使用量

  // Dummy head of LRU list.
  // lru.prev is newest entry, lru.next is oldest entry.
  // Entries have refs==1 and in_cache==true.
  // 穩定狀態下 所有元素都存在這個鏈表中 這個鏈表中元素refs一定等於1
  LRUHandle lru_;

  // Dummy head of in-use list.
  // Entries are in use by clients, and have refs >= 2 and in_cache==true.
  // 當用戶查詢某條記錄時 會將元素從lru_移動到這個鏈表中  這個鏈表中元素refs一定大於等於2  當使用完畢後
  // refs自減 然後將元素移回到lru_中
  LRUHandle in_use_;

  HandleTable table_; /* 元素始終存在hashtable 只有從LRUCache刪除時纔會吧hashtable中元素刪除 */
};

說明:

1)每個LRUCache都有一定容量,當存儲的容量(usage_)超過閾值(capacity_)後就需要刪除舊數據。這一點就是LRU思想。

2)LRUCache有兩個鏈表和一個HashTable(請看註釋內容),分別爲:in_use_,lru_,table_

名稱 作用
in_use_ 所有的新元素都會放到這個鏈表中
lru_ 從in_use_中移除的元素會暫時保存在lru_中
table_ hashTable,所有元素都會存在LRUHandle,用於查詢接口Lookup。

特別說明:

1)一個元素會存儲到hashtable中並且會存儲到某個鏈表中(兩個鏈表中的一個)。

2)新元素一定先存儲到in_use_鏈表中,滿足某些條件後會將元素移植到lru鏈表中,具體看下面分析。

3)當鏈表元素從lru_中刪除,同時需要從hashtable中刪除掉(徹底刪除了)。

 4.1、Insert插入

/**
 * 插入LRUCache中
 * @param key 關鍵字
 * @param hash hash值
 * @param value value值
 * @param charge 
 * @param deleter 回調函數
 */
Cache::Handle* LRUCache::Insert(
    const Slice& key, uint32_t hash, void* value, size_t charge,
    void (*deleter)(const Slice& key, void* value)) {
  MutexLock l(&mutex_);
  //將數據保存在LRUHandle中
  LRUHandle* e = reinterpret_cast<LRUHandle*>(
      malloc(sizeof(LRUHandle)-1 + key.size()));
  e->value = value;
  e->deleter = deleter;
  e->charge = charge;//佔用cache容量 目前始終爲1
  e->key_length = key.size();
  e->hash = hash;
  e->in_cache = false;
  e->refs = 1;  // for the returned handle.
  memcpy(e->key_data, key.data(), key.size());

  if (capacity_ > 0) {
    e->refs++;  // for the cache's reference. 注意這裏refs已經變成2
    e->in_cache = true;
    LRU_Append(&in_use_, e);//插入鏈表
    usage_ += charge; //統計使用量
    FinishErase(table_.Insert(e));//插入hashtable中
  } // else don't cache.  (Tests use capacity_==0 to turn off caching.)

  /* 刪除LRU鏈表中所有節點 */
  while (usage_ > capacity_ && lru_.next != &lru_) {
    LRUHandle* old = lru_.next;
    assert(old->refs == 1);
    bool erased = FinishErase(table_.Remove(old->key(), old->hash));//先從hashtable中刪除
    if (!erased) {  // to avoid unused variable when compiled NDEBUG
      assert(erased);
    }
  }

  return reinterpret_cast<Cache::Handle*>(e);
}

說明:

1)默認capacity一定是大於0的,所以新元素會插入到in_use_鏈表中。

2)table_.Insert是將新元素插入到hashTable中,但是當hashTable中有重複元素(hash值相同&&key值相同)就會把舊元素返回回來。舊元素作爲FinishErase輸入參數,執行後續流程。

3)LRUCache使用場景有兩個地方:open ldb文件和存儲data block時。這兩中場景不太相同,在open ldb文件這種場景下table_.insert返回值一定是null,因爲key是文件編號,leveldb不可能打開同一個文件兩次。 但是對於讀取data block的時候卻有可能出現table_.insert返回非空,比如user-key更新操作,返回飛控。

4)while表示in_use_鏈表存儲已經達到最大,這個時候需要對LRU鏈表進行回收釋放。

4.2、FinishErase

// If e != NULL, finish removing *e from the cache; it has already been removed
// from the hash table.  Return whether e != NULL.  Requires mutex_ held.
bool LRUCache::FinishErase(LRUHandle* e) {
  if (e != NULL) {
    assert(e->in_cache);
    LRU_Remove(e);//從in_use_鏈表中刪除
    e->in_cache = false;
    usage_ -= e->charge; //減小使用計數
    Unref(e);
  }
  return e != NULL;
}

4.3、引用計數器

/**
 * 將引用計數加1
 * @param e
 */
void LRUCache::Ref(LRUHandle* e) {
  if (e->refs == 1 && e->in_cache) {//進入這個分支表示e一定在lru_鏈表中If on lru_ list, move to in_use_ list
    LRU_Remove(e);
    LRU_Append(&in_use_, e);
  }
  e->refs++;
}

/**
 * 釋放引用計數
 * @param e 元素
 * @說明:
 *   要麼直接回收內存 要麼將元素e從in_use_鏈表移動到lru_鏈表
 */
void LRUCache::Unref(LRUHandle* e) {
  assert(e->refs > 0);
  e->refs--;
  if (e->refs == 0) { // Deallocate. 表示沒有引用 直接釋放內存
    assert(!e->in_cache);
    (*e->deleter)(e->key(), e->value);
    free(e);
  } else if (e->in_cache && e->refs == 1) {
    //可參考FindTabe函數 會進入這個分支 No longer in use; move to lru_ list.
    LRU_Remove(e);
    LRU_Append(&lru_, e);//插入LRU鏈表中
  }
}

通常情況下,元素e一定在lru_鏈表中,只有需要使用元素e的時候才把元素e從lru_中移動到in_use_鏈表中,這裏的Unref方法需要注意,這裏是唯一的地方將元素e插入到lru鏈表中。 

五、總結

至此,leveldb的LRUCache相關內容介紹完畢,還剩下一部分方法沒有介紹,大家可自行閱讀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章