leveldb程式碼精讀 lru cache

liiinuuux發表於2015-11-20
檔案
cache.h
cache.cc


LRUCache的最基本的單位是struct LRUHandle,用來存放資料和一系列指標。
LRUCache裡有一個LRUHandle組成的LRU連結串列和一個LRUHandle組成的HandleTable物件存放資料。



先看handle
  1. struct LRUHandle {
  2.   void* value; // 存放資料
  3.   void (*deleter)(const Slice&, void* value); // 用於刪除資料的回撥函式
  4.   LRUHandle* next_hash; // 雜湊捅內的下一個handle
  5.   LRUHandle* next; // 當handle作為lru連結串列的節點時使用
  6.   LRUHandle* prev; // 當handle作為lru連結串列的節點時使用
  7.   size_t charge; // TODO(opt): Only allow uint32_t? // 呼叫者指定的,清理cache時用到。
  8.   size_t key_length;
  9.   uint32_t refs; // 引用計數
  10.   uint32_t hash; // 雜湊值 // Hash of key(); used for fast sharding and comparisons
  11.   char key_data[1]; // 這個和key_length一起組成leveldb自己實現的字串Slice類 // Beginning of key

  12.   // 將key_data, key_length組成leveldb自己實現的字串Slice類,作為key返回
  13.   // 當連結串列裡只有自己,也就是隻把LRUHandle當一個簡單的資料容器時,就返回value。
  14.   Slice key() const {
  15.     // For cheaper lookups, we allow a temporary Handle object
  16.     // to store a pointer to a key in "value".
  17.     if (next == this) {
  18.       return *(reinterpret_cast<Slice*>(value));
  19.     } else {
  20.       return Slice(key_data, key_length);
  21.     }
  22.   }
  23. };


cache不直接儲存LRUHandle,而是將LRUHandle組成HandleTable來儲存
HandleTable主要功能就是維護LRUHandle組成的雜湊表。
私有成員變數:
  1. private:
  2.   // The table consists of an array of buckets where each bucket is
  3.   // a linked list of cache entries that hash into the bucket.
  4.   uint32_t length_; // 雜湊捅的個數
  5.   uint32_t elems_; // 總的handle個數
  6.   LRUHandle** list_; // 指向若干handle的指標陣列,每handle是一個連結串列,俗稱雜湊桶,hash bucket。


下面看這個雜湊表怎麼實現的
  1. /*
  2. 三個私有成員變數置空,函式內容只有一句 Resize(),這個是給雜湊表擴容用的。
  3. 根據內部邏輯,現在elems_是0,就直接將list_初始化一個長度為4的指標陣列。
  4. */
  5. HandleTable() : length_(0), elems_(0), list_(NULL) { Resize(); }

看查詢操作最能體現雜湊表的結構
  1. LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
  2.     /*
  3.     首先建立一個臨時的指標,根據要查詢雜湊值定位雜湊捅,也就是指標陣列list_的一個元素。
  4.     透過雜湊值和list_元素數(length_ - 1)進行"與"運算,可以隨機得到一個小於等於(length_ - 1)的整數,這樣可以高效定位雜湊桶。
  5.     所以說在沒有雜湊碰撞的情況下,雜湊表的查詢效率非常高。
  6.     由於list_是指標陣列,每個元素都是一個指標。list_[hash & (length_ - 1)]本身就是一個指標。
  7.     為了能在函式外面直接透過ptr維護list_,需要將ptr定義為一個二級指標指向list_中某個指標的地址。
  8.     */
  9.     LRUHandle** ptr = &list_[hash & (length_ - 1)];

  10.     /*
  11.     每一個雜湊桶是一個連結串列
  12.     定位到第雜湊表的某一個連結串列之後,在這一個連結串列裡查詢,找到雜湊值和具key值都一樣的,指標就不再移動了。
  13.     這裡分兩組情況
  14.     1 連結串列的第一個handle就是要找的,指標不需要移動,這時ptr是執行這個handle指標的指標。
  15.     2 ptr需要在連結串列裡移動,那麼最終ptr會定位到連結串列裡某個handle的next_hash這個指標。
  16.       這裡又有兩種情況,就是如果next_hash指向的handle就是要找的handle,另一種情況是沒有找到,這時next_hash執行的是NULL。
  17.     但不管怎樣,將來對(*ptr)的賦值操作,就是改變handle或者handle->next_hash這兩個指標的指向。
  18.     */
  19.     while (*ptr != NULL &&
  20.            ((*ptr)->hash != hash || key != (*ptr)->key())) {
  21.       ptr = &(*ptr)->next_hash; // next_hash執行的就是雜湊值相同的下一個handle,也就是這一行的下一個。
  22.     }
  23.     return ptr; // 返回二級指標ptr,讓函式外面可以透過操作ptr來操作雜湊表的資料。
  24.   }


插入操作
  1. LRUHandle* Insert(LRUHandle* h) {
  2.     /*
  3.     先查詢,可以定位到相同資料的已有handle。
  4.     這裡返回的是二級指標,指向了指向list_裡某個雜湊桶的第一個handle指標的指標,或者是指向這個雜湊桶裡某個handle的next_hash的指標。
  5.     如果雜湊表裡沒有相同handle,ptr會指向這個雜湊桶最後一個handle的next_hash這個指標本身。
  6.     因此操作*ptr就是操作雜湊桶本身。
  7.     */
  8.     LRUHandle** ptr = FindPointer(h->key(), h->hash);

  9.     /*
  10.     ptr是二級指標,因此*ptr是找到的handle的前一個handle的next_hash這個指標。
  11.     因此old被賦值後,就是找到的handle
  12.     */
  13.     LRUHandle* old = *ptr;
  14.     /*
  15.     leveldb裡的容器有一個特點,就是不允許重複值
  16.     old->next_hash指的是找到的handle的next_hash
  17.     讓h->next_hash指向old的next_hash,再讓*ptr指向h,其實是把找到的handle從連結串列裡摘掉了,用h替換old。
  18.     *ptr是已有handle的next_hash。*ptr = h的操作實際上是讓已有handle的上一個handle的next_hash指向h.
  19.     */
  20.     h->next_hash = (old == NULL ? NULL : old->next_hash);
  21.     *ptr = h;

  22.     // 如果沒找到重複值,就新建一個handle,並且按需要對雜湊表擴容
  23.     if (old == NULL) {
  24.       ++elems_;
  25.       /*
  26.       每次插入後,判斷handle數是否大於行數。
  27.       如果handle數大於雜湊桶數,則呼叫resize函式對雜湊表進行擴容,擴充雜湊桶數,保證每個雜湊桶最多一條記錄。
  28.       */
  29.       if (elems_ > length_) {
  30.         // Since each cache entry is fairly large, we aim for a small
  31.         // average linked list length (<= 1).
  32.         Resize();
  33.       }
  34.     }
  35.     // 將old返回很重要,因為這個被摘到的handle需要在函式外面銷燬。
  36.     return old;
  37.   }


刪除和插入操作原理類似,很簡單
  1. LRUHandle* Remove(const Slice& key, uint32_t hash) {
  2.     LRUHandle** ptr = FindPointer(key, hash);
  3.     LRUHandle* result = *ptr;
  4.     if (result != NULL) {
  5.       *ptr = result->next_hash;
  6.       --elems_;
  7.     }
  8.     return result;
  9.   }


對雜湊表的擴容,就是新建一個雜湊桶數更多的list_,將所有handle重新排布到更多的雜湊桶裡。
  1. void Resize() {
  2.     // 初始化時是4個雜湊桶
  3.     uint32_t new_length = 4;
  4.     //重新決定雜湊桶的數量
  5.     while (new_length < elems_) {
  6.       new_length *= 2;
  7.     }
  8.     LRUHandle** new_list = new LRUHandle*[new_length];
  9.     memset(new_list, 0, sizeof(new_list[0]) * new_length);
  10.     uint32_t count = 0;
  11.     for (uint32_t i = 0; i < length_; i++) {
  12.       LRUHandle* h = list_[i];
  13.       while (h != NULL) {
  14.         LRUHandle* next = h->next_hash;
  15.         uint32_t hash = h->hash;
  16.         // 很據先有handle的雜湊值定位新的雜湊桶
  17.         LRUHandle** ptr = &new_list[hash & (new_length - 1)];
  18.         /*
  19.         下面是二級指標操作
  20.         1 雜湊桶是空的的時候,*ptr指向NULL,讓h->next_hash指向NULL
  21.         2 當雜湊桶裡有東西的時候,*ptr指向雜湊桶裡第一個handle,在這個函式里就是上一次迴圈的h
  22.             讓h->next_hash指向雜湊桶第一個handle
  23.             讓指向雜湊桶第一個handle的指標指向h
  24.         */
  25.         h->next_hash = *ptr;
  26.         *ptr = h;

  27.         // h在老雜湊桶裡向後移動
  28.         h = next;
  29.         count++; // 新雜湊表裡的handle數加1
  30.       }
  31.     }
  32.     assert(elems_ == count);

  33.     刪除老的指標陣列,將list_指向新的。
  34.     delete[] list_;
  35.     list_ = new_list;
  36.     length_ = new_length;
  37.   }
  38. };


LRUCache
最重要的兩個私有變數
  1. // 由handle組成的lru連結串列,最近訪問的資料在最前端。
  2.   LRUHandle lru_;
  3.   // 用雜湊表存放資料
  4.   HandleTable table_;


查詢操作非常簡單,就是調table_.Lookup,而table_.Lookup只是對FindPointer的簡單呼叫
  1. Cache::Handle* LRUCache::Lookup(const Slice& key, uint32_t hash) {
  2.   MutexLock l(&mutex_);
  3.   LRUHandle* e = table_.Lookup(key, hash);

  4.   // cache的查詢比雜湊表多兩點,一個是更新handle的引用計數,另一個是將handle從lru摘除,加到最近訪問的頂端。
  5.   if (e != NULL) {
  6.     e->refs++;
  7.     LRU_Remove(e);
  8.     LRU_Append(e);
  9.   }
  10.   return reinterpret_cast<Cache::Handle*>(e);
  11. }


LRU_Remove和LRU_Append都是簡單的指標操作
下面是LRU_Append。leveldb的lru是迴圈連結串列,規則是:表頭的next是最冷端,表頭的priv是最近訪問的。
  1. void LRUCache::LRU_Append(LRUHandle* e) {
  2.   // Make "e" newest entry by inserting just before lru_
  3.   e->next = &lru_;
  4.   e->prev = lru_.prev;
  5.   e->prev->next = e;
  6.   e->next->prev = e;
  7. }



LRUCache裡值得一看的是insert操作。
  1. Cache::Handle* LRUCache::Insert(
  2.     const Slice& key, uint32_t hash, void* value, size_t charge,
  3.     void (*deleter)(const Slice& key, void* value)) {
  4.   MutexLock l(&mutex_); // 保證執行緒安全,先獲取mutex

  5.   // 將引數封裝成handle物件。
  6.   LRUHandle* e = reinterpret_cast<LRUHandle*>(
  7.       malloc(sizeof(LRUHandle)-1 + key.size()));
  8.   e->value = value;
  9.   e->deleter = deleter;
  10.   e->charge = charge;
  11.   e->key_length = key.size();
  12.   e->hash = hash;
  13.   e->refs = 2; // One from LRUCache, one for the returned handle
  14.   memcpy(e->key_data, key.data(), key.size());

  15.   // 新handle加到lru頂端
  16.   LRU_Append(e);
  17.   usage_ += charge;

  18.   LRUHandle* old = table_.Insert(e);
  19.   // table_.insert 用e替換老的old,old需要手動銷燬。
  20.   if (old != NULL) {
  21.     LRU_Remove(old);
  22.     Unref(old);
  23.   }

  24.   /*
  25.   下面這段是LRUCache的關鍵
  26.   lru連結串列的結構比較特殊,lru_.next永遠指向最冷,最長時間沒人訪問的handle
  27.   每次插入新handle前,呼叫者會手工指定charge。
  28.   插入時usage_會+=handle的charge,
  29.   當usage_超過呼叫者定義的容量(capacity_)時就要從冷端開始清理資料
  30.   這個機制可以讓呼叫者透過控制capacity_和新handle的charge來調整lru連結串列的清理行為
  31.   */
  32.   while (usage_ > capacity_ && lru_.next != &lru_) {
  33.     LRUHandle* old = lru_.next;
  34.     LRU_Remove(old);
  35.     table_.Remove(old->key(), old->hash);
  36.     Unref(old);
  37.   }

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










來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26239116/viewspace-1842049/,如需轉載,請註明出處,否則將追究法律責任。

相關文章