LSM-Tree - LevelDb之LRU快取

lazytimes發表於2022-07-11

LSM-Tree - LevelDb之LRU快取

引言

LRU快取在各種開源元件中都有使用的場景,常常用於做冷熱資料和淘汰策略,使用LRU主要有三點。

  1. 第一點是實現非常簡單。
  2. 第二點是程式碼量本身也不錯。
  3. 最後涉及資料結構非常經典。

LevelDB對於LRU快取實現算是比較經典的案例,這一節來介紹它是如何使用LRU實現快取的。

LeetCode 中有一道相應LRU快取演算法的題目,感興趣可以做一做:
lru-cache

理論

根據wiki的LRU快取結構介紹,可以瞭解到快取的基本淘汰策略過程,比如下面這張圖的節點淘汰過程:

讀取的順序為 A B C D E D F,快取大小為 4,括號內的數字表示排序,數字越小越靠後,表示 Least recently.

根據箭頭的讀取順序,讀取到E的時候,發現快取已經滿了,這時會淘汰最早的一個A(0)。

接下來繼續讀取並且更新排序,倒數第二次中發現D是最大的,而B是最小的,當讀取F加入快取之後,發現快取已經是滿的,此時發現相對於A之後插入的數值B訪問次數最小,於是進行淘汰並且替換。

根據最少實用原則LRU 的實現需要兩個資料結構:

  1. HashTable(雜湊表): 用於實現O(1)的查詢。
  2. List: 儲存 Least recently 排序,用於舊資料的淘汰。

LevelDB實現

這裡直接看LevelDB是如何應用這個資料結構的。

LevelDB的LRUCache設計有4個資料結構,是依次遞進的關係,分別是:

  • LRUHandle(LRU節點,也就是LRUNode)
  • HandleTable(雜湊表)
  • LRUCache(關鍵,快取介面標準和實現)
  • ShardedLRUCache(用於提高併發效率)

整個LevelDB 的資料結構組成如下:

下面是相關介面定義:

// 插入一個鍵值對(key,value)到快取(cache)中,
// 並從快取總容量中減去該鍵值對所佔額度(charge) 
// 
// 返回指向該鍵值對的控制程式碼(handle),呼叫者在用完控制程式碼後,
// 需要呼叫 this->Release(handle) 進行釋放
//
// 在鍵值對不再被使用時,鍵值對會被傳入的 deleter 引數
// 釋放
virtual Handle* Insert(const Slice& key, void* value, size_t charge,
                       void (*deleter)(const Slice& key, void* value)) = 0;

// 如果快取中沒有相應鍵(key),則返回 nullptr
//
// 否則返回指向對應鍵值對的控制程式碼(Handle)。呼叫者用完控制程式碼後,
// 要記得呼叫 this->Release(handle) 進行釋放
virtual Handle* Lookup(const Slice& key) = 0;

// 釋放 Insert/Lookup 函式返回的控制程式碼
// 要求:該控制程式碼沒有被釋放過,即不能多次釋放
// 要求:該控制程式碼必須是同一個例項返回的
virtual void Release(Handle* handle) = 0;

// 獲取控制程式碼中的值,型別為 void*(表示任意使用者自定義型別)
// 要求:該控制程式碼沒有被釋放
// 要求:該控制程式碼必須由同一例項所返回
virtual void* Value(Handle* handle) = 0;

// 如果快取中包含給定鍵所指向的條目,則刪除之。
// 需要注意的是,只有在所有持有該條目控制程式碼都釋放時,該條目所佔空間才會真正被釋放
virtual void Erase(const Slice& key) = 0;

// 返回一個自增的數值 id。當一個快取例項由多個客戶端共享時,
// 為了避免多個客戶端的鍵衝突,每個客戶端可能想獲取一個獨有
// 的 id,並將其作為鍵的字首。類似於給每個客戶端一個單獨的名稱空間。
virtual uint64_t NewId() = 0;

// 驅逐全部沒有被使用的資料條目
// 記憶體吃緊型的應用可能想利用此介面定期釋放記憶體。
// 基類中的 Prune 預設實現為空,但強烈建議所有子類自行實現。
// 將來的版本可能會增加一個預設實現。
virtual void Prune() {}

// 返回當前快取中所有資料條目所佔容量總和的一個預估
virtual size_t TotalCharge() const = 0;

根據LevelDB介面定義,可以整理出快取解決了如下的需求:

  1. 多執行緒支援
  2. 效能需求
  3. 資料條目的生命週期管理

cache.cc

在cache.cc中基本包含了LevelDB關於LRU快取實現的所有程式碼。

HandleTable - 雜湊表

HandleTableHashTable 其實就是名字的區別,內部比較關鍵的幾個屬性如下。

private:

// The table consists of an array of buckets where each bucket is

// a linked list of cache entries that hash into the bucket.
// 第一個length_ 儲存了桶的個數
uint32_t length_;
// 維護在整個hash表中一共存放了多少個元素
uint32_t elems_;
// 二維指標,每一個指標指向一個桶的表頭位置
LRUHandle** list_;

為了提升查詢效率,一個桶裡面儘可能只有一個元素,在下面的插入程式碼中使用了二級指標的方式處理,但是實際上並不算非常複雜,插入的時候會找到前驅節點,並且操作的是前驅節點中的 next_hash 指標:

// 首先讀取 `next_hash`,找到下一個鏈節,將其鏈到待插入節點後邊,然後修改前驅節點 `next_hash` 指向。
LRUHandle* Insert(LRUHandle* h) {
    // 查詢節點,路由定位
    LRUHandle** ptr = FindPointer(h->key(), h->hash);
    
    LRUHandle* old = *ptr;
    
    h->next_hash = (old == nullptr ? nullptr : old->next_hash);
    
    *ptr = h;
    
    if (old == nullptr) {
    
        ++elems_;
    
        if (elems_ > length_) {
        
            // Since each cache entry is fairly large, we aim for a small average linked list length (<= 1).
            // 由於每個快取條目都相當大,我們的目標是一個小的平均連結串列長度(<= 1)。
            Resize();
        }
    }
    return old;

}

另外當整個hash表中元素的個數超過 hash表桶的的個數的時候,會呼叫Resize函式並且把整個桶的個數增加一倍,同時將現有的元素搬遷到合適的桶的後面。

注意這個resize的操作來自一篇[[Dynamic-sized NonBlocking Hash table]]的文章,作者參照此論文的敘述設計了整個雜湊表,其中最為關鍵的部分就是resize的部分,關於這部分內容將單獨通過一篇文章進行分析。

如果想要了解具體到演算法理論,那麼必然需要了解上面提到的論文:

文件下載:p242-liu.pdf

    /*
        Resize 操作
    */
    void Resize() {
    
        uint32_t new_length = 4;
        
        while (new_length < elems_) {
            // 擴充套件
            new_length *= 2;
    
        }
        
        LRUHandle** new_list = new LRUHandle*[new_length];
        // 遷移雜湊表
        memset(new_list, 0, sizeof(new_list[0]) * new_length);
        
        uint32_t count = 0;
        
        for (uint32_t i = 0; i < length_; i++) {
            
            LRUHandle* h = list_[i];
            
            while (h != nullptr) {
            
                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++;
        
            }
    
        }
    
        assert(elems_ == count);
        
        delete[] list_;
        
        list_ = new_list;
        
        length_ = new_length;
    
    }

};

這種類似集合提前擴容的方式,結合良好的hash函式以及之前作者提到的元素長度控制為直接一倍擴容,最終這種優化之後的查詢的效率可以認為為O(1)

這裡展開說一下FindPointer定位路由的方法,可以看到這裡通過快取節點的hash值和位操作快速找到對應二級指標節點,也就是找到最終的桶,同時因為連結串列內部沒有排序,這裡通過全連結串列遍歷的方式找到節點。

除此之外還有一個細節是上面的插入使用的是前驅接節點的 next_hash,這裡查詢返回這個物件也是合適的。

最終的節點查詢過程如下:

  1. 如果節點的 hash 或者 key 匹配上,則返回該節點的雙重指標(前驅節點的 next_hash 指標的指標)。
  2. 否則返回該連結串列的最後一個節點的雙重指標(邊界情況,如果是空連結串列,最後一個節點便是桶頭)。
// 返回一個指向 slot 的指標,該指標指向一個快取條目
// 匹配鍵/雜湊。 如果沒有這樣的快取條目,則返回一個
// 指向對應連結串列中尾隨槽的指標。
LRUHandle** FindPointer(const Slice& key, uint32_t hash) {

    LRUHandle** ptr = &list_[hash & (length_ - 1)];
    
    while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) {
    
        ptr = &(*ptr)->next_hash;
    
    }
    
    return ptr;

}

刪除操作會修改 next_hash 的指向,和新增的基本操作類似,這裡不多介紹了。

在刪除時只需將該 next_hash 改為待刪除節點後繼節點地址,然後返回待刪除節點即可。

    LRUHandle* Remove(const Slice& key, uint32_t hash) {
    
        LRUHandle** ptr = FindPointer(key, hash);
        
        LRUHandle* result = *ptr;
        
        if (result != nullptr) {
        
            *ptr = result->next_hash;
            
            --elems_;
        
        }
        
        return result;

    }

小結

整個雜湊表的內容發現主要的難點在理解二級指標的維護上,在這個幾個方法當中副作用最大的函式是Resize方法,此方法在執行的時候會鎖住整個雜湊表並且其他執行緒必須等待重新分配雜湊表結束,雖然一個桶儘量指向一個節點,但是如果雜湊分配不均勻依然會有效能影響。

另外需要注意雖然需要阻塞,但是整個鎖的最小粒度是桶,大部分情況下其他執行緒讀取其他的桶並沒有影響。

再次強調,為解決問題併發讀寫的雜湊表,在這裡,提到一種漸進式的遷移方法:Dynamic-sized NonBlocking Hash table,可以將遷移時間進行均攤,有點類似於 Go GC 的演化。

"Dynamic-Sized Nonblocking Hash Tables", by Yujie Liu, Kunlong Zhang, and Michael Spear. ACM Symposium on Principles of Distributed Computing, Jul 2014.=

根據理論知識可以知道,LRU快取的淘汰策略通常會把最早訪問的資料移除快取,但是顯然通過雜湊表是無法完成這個操作的,所以我們需要從快取節點看到設計。

LRUCache - LRU快取

LRU快取的大致結構如下:

和多數的LRU快取一樣,2個連結串列的含義是保證冷熱分離的形式,內部維護雜湊表。

  • lru_:冷連結串列,如果引用計數歸0移除“冷宮”。
  • in_use_:熱連結串列,通過引用計數通常從冷連結串列加入到熱連結串列。
  LRUHandle lru_;      // lru_ 是冷連結串列,屬於冷宮,

  LRUHandle in_use_;   // in_use_ 屬於熱連結串列,熱資料在此連結串列

  HandleTable table_; // 雜湊表部分已經講過

LRUCache中將之前分析過的匯出介面 Cache 所包含的函式省略後,LRUCache 類簡化如下:

class LRUCache {
 public:
  LRUCache();
  ~LRUCache();

  // 構造方法可以手動之處容量,
  void SetCapacity(size_t capacity) { capacity_ = capacity; }

 private:
  // 輔助函式:將鏈節 e 從雙向連結串列中摘除
  void LRU_Remove(LRUHandle* e);
  // 輔助函式:將鏈節 e 追加到連結串列頭
  void LRU_Append(LRUHandle* list, LRUHandle* e);
  // 輔助函式:增加鏈節 e 的引用
  void Ref(LRUHandle* e);
  // 輔助函式:減少鏈節 e 的引用
  void Unref(LRUHandle* e);
  // 輔助函式:從快取中刪除單個鏈節 e
  bool FinishErase(LRUHandle* e) EXCLUSIVE_LOCKS_REQUIRED(mutex_);

  // 在使用 LRUCache 前必須先初始化此值
  size_t capacity_;

  // mutex_ 用以保證此後的欄位的執行緒安全
  mutable port::Mutex mutex_;
  size_t usage_ GUARDED_BY(mutex_);

  // lru 雙向連結串列的空表頭
  // lru.prev 指向最新的條目,lru.next 指向最老的條目
  // 此連結串列中所有條目都滿足 refs==1 和 in_cache==true
  // 表示所有條目只被快取引用,而沒有客戶端在使用

    // 作者註釋
    // LRU 列表的虛擬頭。
   // lru.prev 是最新條目,lru.next 是最舊條目。
   // 條目有 refs==1 和 in_cache==t
  LRUHandle lru_ GUARDED_BY(mutex_);

  // in-use 雙向連結串列的空表頭
  // 儲存所有仍然被客戶端引用的條目
  // 由於在被客戶端引用的同時還被快取引用,
  // 肯定有 refs >= 2 和 in_cache==true.

  // 作者註釋:
  // 使用中列表的虛擬頭。
  // 條目正在被客戶端使用,並且 refs >= 2 並且 in_cache==true。
  LRUHandle in_use_ GUARDED_BY(mutex_);

  // 所有條目的雜湊表索引
  HandleTable table_ GUARDED_BY(mutex_);
};

冷熱連結串列通過下面的方法進行維護:

  • Ref: 表示函式要使用該cache,如果對應元素位於冷連結串列,需要將它從冷連結串列溢位鏈入到熱連結串列。
  • Unref:和Ref相反,表示客戶不再訪問該元素,需要將引用計數-1,再比如徹底沒人用了,引用計數為0就可以刪除這個元素了,如果引用計數為1,則可以將元素打入冷宮放入到冷連結串列。
    void LRUCache::Ref(LRUHandle* e) {
    
        if (e->refs == 1 && e->in_cache) { 
            // If on lru_ list, move to in_use_ list.
            
            LRU_Remove(e);
            // 打入熱連結串列
            LRU_Append(&in_use_, e);
        
        }
        
        e->refs++;
    
    }

    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) {
            
            // No longer in use; move to lru_ list.
            // 不再使用; 移動到 冷連結串列。
            LRU_Remove(e);
            
            LRU_Append(&lru_, e);
        
        }
    
    }

LRU 新增和刪除節點比較簡單,和一般的連結串列操作類似:

    void LRUCache::LRU_Append(LRUHandle* list, LRUHandle* e) {
        // 通過在 *list 之前插入來建立“e”最新條目
        // Make "e" newest entry by inserting just before *list
        
        e->next = list;
        
        e->prev = list->prev;
        
        e->prev->next = e;
        
        e->next->prev = e;
    
    }
    
    void LRUCache::LRU_Remove(LRUHandle* e) {
        
        e->next->prev = e->prev;
        
        e->prev->next = e->next;
    
    }

因為是快取,所以有容量的限制,如果超過容量就必須要從冷連結串列當中剔除訪問最少的元素。


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* e =
    
    reinterpret_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
    
    e->value = value;
    
    e->deleter = deleter;
    
    e->charge = charge;
    
    e->key_length = key.size();
    
    e->hash = hash;
    
    e->in_cache = false;
    
    e->refs = 1; // for the returned handle. 返回的控制程式碼
    
    std::memcpy(e->key_data, key.data(), key.size());
    
      
    
    if (capacity_ > 0) {
    
        e->refs++; // for the cache's reference.
        
        e->in_cache = true;
        //鏈入熱連結串列        
        LRU_Append(&in_use_, e);
        //使用的容量增加
        usage_ += charge;
        // 如果是更新的話,需要回收老的元素
    
        FinishErase(table_.Insert(e));
    
    } else { 
        // don't cache. (capacity_==0 is supported and turns off caching.)
        
         // capacity_==0 時表示關閉快取,不進行任何快取

        // next is read by key() in an assert, so it must be initialized
        // next 由斷言中的 key() 讀取,因此必須對其進行初始化
        e->next = nullptr;
    
    }
    
    while (usage_ > capacity_ && lru_.next != &lru_) {
    //如果容量超過了設計的容量,並且冷連結串列中有內容,則從冷連結串列中刪除所有元素
    
        LRUHandle* old = lru_.next;
        
        assert(old->refs == 1);
        
        bool erased = FinishErase(table_.Remove(old->key(), old->hash));
        
        if (!erased) { // to avoid unused variable when compiled NDEBUG
        
            assert(erased);
        
        }
    
    }
    
      
    
    return reinterpret_cast<Cache::Handle*>(e);

}

這裡需要關注下面的程式碼:

    if (capacity_ > 0) {
    
        e->refs++; // for the cache's reference.
        
        e->in_cache = true;
        //鏈入熱連結串列        
        LRU_Append(&in_use_, e);
        //使用的容量增加
        usage_ += charge;
        // 如果是更新的話,需要回收老的元素
        FinishErase(table_.Insert(e));
    
    } else { 
        // don't cache. (capacity_==0 is supported and turns off caching.)
        // 不要快取。 (容量_==0 受支援並關閉快取。)
        // next is read by key() in an assert, so it must be initialized
        // next 由斷言中的 key() 讀取,因此必須對其進行初始化
        e->next = nullptr;
    
    }

在內部的程式碼需要注意FinishErase(table_.Insert(e));方法,這部分程式碼需要結合之前介紹的雜湊表(HandleTable)的LRUHandle* Insert(LRUHandle* h)方法,,在內部如果發現同一個key值的元素已經存在,HandleTable的Insert函式會將舊的元素返回。

因此LRU的Insert函式內部隱含了更新的操作,會將新的Node加入到Cache中,而老的元素會呼叫FinishErase函式來決定是移入冷宮還是徹底刪除

// If e != nullptr, finish removing *e from the cache; it has already been removed from the hash table. Return whether e != nullptr.
// 如果e != nullptr,從快取中刪除*e;表示它已經被從雜湊表中刪除。同時返回e是否 !=nullptr。
bool LRUCache::FinishErase(LRUHandle* e) {

    if (e != nullptr) {
    
        assert(e->in_cache);
        
        LRU_Remove(e);
        
        e->in_cache  = false;
        
        usage_ -= e->charge;
        // 狀態改變
        Unref(e);
    
    }
    
    return e != nullptr;

}
擴充套件:Mysql的記憶體快取頁也是使用冷熱分離的方式進行維護和處理,目的使平衡可用頁和快取命中,但是我們都知道8.0快取直接刪掉了,原因是現今稍大的OLTP業務對於快取的命中率非常低。

小結

LevelDB當中到LRU快取實現有以下特點:

  1. 使用冷熱分離連結串列維護資料,冷熱資料之間的資料不存在交集:被客戶端引用的 in-use 連結串列,和不被任何客戶端引用的 lru_ 連結串列。
  2. 雙向連結串列的設計,同時其中一端使用空連結串列作為邊界判斷。表頭的 prev 指標指向最新的條目,next 指標指向最老的條目,最終形成了雙向環形連結串列
  3. 使用 usage_ 表示快取當前已用量,用 capacity_ 表示該快取總量。
  4. 抽象出了幾個基本操作:LRU_RemoveLRU_AppendRefUnref 作為輔助函式進行復用。
  5. 每個 LRUCache 由一把鎖 mutex_ 守護。

LRUHandle - LRU節點

LRU節點 通過狀態判斷切換是否存在快取當中,如果引用計數為0,則通過erased方法被移除雜湊以及LRU的連結串列。

下面是具體的示意圖:

LRUHandle 其實就是快取節點LRUNode,LevelDB的Cache管理,通過作者的註釋可以瞭解到,整個快取節點維護有2個雙向連結串列和一個雜湊表,雜湊表不需要過多介紹。

雜湊的經典問題就是雜湊碰撞,而使用連結串列節點解決雜湊節點的問題是經典的方式,LevelDB也不例外,不過他要比傳統的設計方式要複雜一點。

// 機翻自作者的註釋,對於理解作者設計比較關鍵,翻得一般。建議對比原文多讀幾遍
// LRU快取實現
//
// 快取條目有一個“in_cache”布林值,指示快取是否有
// 對條目的引用。如果沒有傳遞給其“刪除器”的條目是通過 Erase(),
// 通過 Insert() 時, 插入具有重複鍵的元素,或在快取銷燬時。
//
// 快取在快取中儲存兩個專案的連結串列。中的所有專案
// 快取在一個列表或另一個列表中,並且永遠不會同時存在。仍被引用的專案
// 由客戶端但從快取中刪除的不在列表中。名單是:

// - in-use: 包含客戶端當前引用的專案,沒有
// 特定順序。 (這個列表用於不變檢查。如果我們
// 刪除檢查,否則該列表中的元素可能是
// 保留為斷開連線的單例列表。)

// - LRU:包含客戶端當前未引用的專案,按 LRU 順序
// 元素通過 Ref() 和 Unref() 方法在這些列表之間移動,
// 當他們檢測到快取中的元素獲取或丟失它的唯一
// 外部參考。

// 一個 Entry 是一個可變長度的堆分配結構。 條目儲存在按訪問時間排序的迴圈雙向連結串列中。
// LRU cache implementation

//

// Cache entries have an "in_cache" boolean indicating whether the cache has a

// reference on the entry. The only ways that this can become false without the

// entry being passed to its "deleter" are via Erase(), via Insert() when

// an element with a duplicate key is inserted, or on destruction of the cache.

//

// The cache keeps two linked lists of items in the cache. All items in the

// cache are in one list or the other, and never both. Items still referenced

// by clients but erased from the cache are in neither list. The lists are:

// - in-use: contains the items currently referenced by clients, in no

// particular order. (This list is used for invariant checking. If we

// removed the check, elements that would otherwise be on this list could be

// left as disconnected singleton lists.)

// - LRU: contains the items not currently referenced by clients, in LRU order

// Elements are moved between these lists by the Ref() and Unref() methods,

// when they detect an element in the cache acquiring or losing its only

// external reference.

// An entry is a variable length heap-allocated structure. Entries

// are kept in a circular doubly linked list ordered by access time.
struct LRUHandle {

    void* value;
    
    void (*deleter)(const Slice&, void* value); // 釋放 key value 空間使用者回撥
        
    LRUHandle* next_hash; // 用於 Hashtable 處理連結串列衝突
    
    LRUHandle* next; // 雙向連結串列維護LRU順序
    
    LRUHandle* prev;
    
    size_t charge; // TODO(opt): Only allow uint32_t?
    
    size_t key_length;
    
    bool in_cache; // 該 handle 是否在 cache table 中

    
    uint32_t refs; // 該 handle 被引用次數
    
    uint32_t hash; // key 的 hash值,
    
    char key_data[1]; // Beginning of key
    
    Slice key() const {
    
    // next is only equal to this if the LRU handle is the list head of an
    
    // empty list. List heads never have meaningful keys.
    // 僅當 LRU 控制程式碼是空列表的列表頭時,next 才等於此值。此時列表頭永遠不會有有意義的鍵。
    assert(next != this);
    
    return Slice(key_data, key_length);

}

ShardedLRUCache

該類繼承Cache介面,並且和所有的LRUCache一樣都會加鎖,但是不同的是ShardedLRUCache可以定義多個LRUCache分別處理不同的hash取模之後的快取處理。

class ShardedLRUCache : public Cache {

    private:
    
    LRUCache shard_[kNumShards];
    
    port::Mutex id_mutex_;
    
    uint64_t last_id_;
    
    static inline uint32_t HashSlice(const Slice& s) {
    
        return Hash(s.data(), s.size(), 0);
    
    }
    
    static uint32_t Shard(uint32_t hash) { return hash >> (32 - kNumShardBits); }
    
    public:
    
        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);
        
        }
    
    }
    
    ~ShardedLRUCache() override {}
    
    Handle* Insert(const Slice& key, void* value, size_t charge,
    
            void (*deleter)(const Slice& key, void* value)) override {
    
        const uint32_t hash = HashSlice(key);
        
        return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
    
    }
    
    Handle* Lookup(const Slice& key) override {
    
        const uint32_t hash = HashSlice(key);
        
        return shard_[Shard(hash)].Lookup(key, hash);
    
    }
    
    void Release(Handle* handle) override {
    
        LRUHandle* h = reinterpret_cast<LRUHandle*>(handle);
        
        shard_[Shard(h->hash)].Release(handle);
    
    }
    
    void Erase(const Slice& key) override {
    
        const uint32_t hash = HashSlice(key);
        
        shard_[Shard(hash)].Erase(key, hash);
    
    }
    
    void* Value(Handle* handle) override {
    
        return reinterpret_cast<LRUHandle*>(handle)->value;
    
    }

    // 全域性唯一自增ID
    uint64_t NewId() override {
    
        MutexLock l(&id_mutex_);
        
        return ++(last_id_);
    
    }
    
    void Prune() override {
    
        for (int s = 0; s < kNumShards; s++) {
        
            shard_[s].Prune();
        
        }
    
    }
    
    size_t TotalCharge() const override {
    
    size_t total = 0;
    
    for (int s = 0; s < kNumShards; s++) {
    
        total += shard_[s].TotalCharge();
    
    }
    
    return total;
    
    }

};

ShardedLRUCache 內部有16個LRUCache,查詢key的時候,先計算屬於哪一個LRUCache,然後在相應的LRUCache中上鎖查詢,這種策略並不少見,但是核心的程式碼並不在這一部分,所以放到了最後進行介紹。

16個LRUCache,Shard 方法利用 key 雜湊值的前 kNumShardBits = 4 個 bit 作為分片路由,最終可以支援 kNumShards = 1 << kNumShardBits 16 個分片,這也是16個分片

static uint32_t Shard(uint32_t hash) { return hash >> (32 - kNumShardBits); }

由於 LRUCache 和 ShardedLRUCache 都實現了 Cache 介面,因此 ShardedLRUCache 只需將所有 Cache 介面操作路由到對應 Shard 即可,總體來說 ShardedLRUCache 沒有太多邏輯,這裡不再贅述。

總結

整個LevelDB的LRU快取實現,除了在起名的地方有點非主流之外,基本符合LRU的設計思想。

整個LevelDB的核心是雜湊表和雜湊函式,支援併發讀寫的雜湊表以及resize函式核心部分都是值得推敲。

關於雜湊表的優化實際上自出現開始就一直在優化,LevelDB上的實現是一個不錯的參考。

寫在最後

LevelDB比較重要的元件基本介紹完成,LevelDB的LRU快取也可以看做是教科書一般的實現。

相關文章