leveldb學習:Cache,leveldb學習cache
leveldb自己實現了cache緩衝區替代演算法,參見代碼cache.h和cache.c檔案。leveldb中table_cache等都是以class cache作為底層實現。
cache.h中,我們看到cache類是一個抽象類別,聲明了lookup;insert;release;value;erase等函數,同時聲明了一個全域函數
extern Cache* NewLRUCache(size_t capacity);
用來構造cache衍生類別對象,並返回衍生類別指標。那麼cache的衍生類別究竟是什麼呢?很容易在cache.cc中發現了ShardedLRUCache類,繼承自cache,這是leveldb緩衝區演算法的預設實現。
ShardedLRUCache成員變數static const int kNumShardBits = 4;static const int kNumShards = 1 << kNumShardBits; private: LRUCache shard_[kNumShards]; //暫時不明 port::Mutex id_mutex_; //互斥鎖 uint64_t last_id_; //不明
LRUCache的實現我們暫且不知道,但看起來ShardedLRUCache應該是一個封裝類,真正的cache是LRUCache,而且是16個。先看ShardedLRUCache函數:
//返回key的hash值 static inline uint32_t HashSlice(const Slice& s) { return Hash(s.data(), s.size(), 0); } //取hash的前四位 static uint32_t Shard(uint32_t hash) { return hash >> (32 - kNumShardBits); } public: //構造ShardedLRUCache對象,初始化LRUCache成員變數 //設定容量,並且容量和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); } } virtual ~ShardedLRUCache() { } //插入操作 //先取key的hash值 HashSlice(key),hash值得前四位(Shard(hash))決定key所在的LRUCache數組 //將key插入shard_[Shard(hash)] virtual Handle* Insert(const Slice& key, void* value, size_t charge, void (*deleter)(const Slice& key, void* value)) { const uint32_t hash = HashSlice(key); return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter); } //尋找操作,和插入過程操作邏輯一樣 virtual Handle* Lookup(const Slice& key) { const uint32_t hash = HashSlice(key); return shard_[Shard(hash)].Lookup(key, hash); } virtual void Release(Handle* handle) { LRUHandle* h = reinterpret_cast<LRUHandle*>(handle); shard_[Shard(h->hash)].Release(handle); } virtual void Erase(const Slice& key) { const uint32_t hash = HashSlice(key); shard_[Shard(hash)].Erase(key, hash); } virtual void* Value(Handle* handle) { return reinterpret_cast<LRUHandle*>(handle)->value; } virtual uint64_t NewId() { MutexLock l(&id_mutex_); return ++(last_id_); }
從ShardedLRUCache的成員函數,我們還是獲得了很多關於ShardedLRUCache的資訊。ShardedLRUCache是一個封裝類,真正的cache是LRUCache數組,ShardedLRUCache完成的操作就是計算key的hash值,並以hash值得高四位決定key所在的LRUCache數組,然後調用LRUCache的函數完成cache操作。
LRUCache: // Initialized before use. size_t capacity_; //容量 // mutex_ protects the following state. port::Mutex mutex_; //互斥鎖 size_t usage_; //使用量 // Dummy head of LRU list. // lru.prev is newest entry, lru.next is oldest entry. LRUHandle lru_; //不明 HandleTable table_; //不明 LRUCache成員變數如上,再看看LRUCache的函數//刪除e節點void LRUCache::LRU_Remove(LRUHandle* e) { e->next->prev = e->prev; e->prev->next = e->next;}//附加e節點void LRUCache::LRU_Append(LRUHandle* e) { // Make "e" newest entry by inserting just before lru_ //添加的新節點在lru_之前 e->next = &lru_; e->prev = lru_.prev; e->prev->next = e; e->next->prev = e;}//尋找操作//table_儲存著key所在handle的指標資訊//尋找完要把handle提到鏈表的最前面,是一種為了高效尋找的策略Cache::Handle* LRUCache::Lookup(const Slice& key, uint32_t hash) { MutexLock l(&mutex_); LRUHandle* e = table_.Lookup(key, hash); if (e != NULL) { e->refs++; LRU_Remove(e); LRU_Append(e); } return reinterpret_cast<Cache::Handle*>(e);}
可以確定,LRUCache封裝了一個LRUHandle鏈表的資訊,lru_是這個鏈表的頭結點,table_是一個輔助定位鏈表中各LRUHandle節點的結構。
LRUHandle結構體:
下面我們終於來到了cache的最底層,LRUHandle結構真正包含了所緩衝的資料
struct LRUHandle { //value資料 void* value; //delete函數指標 void (*deleter)(const Slice&, void* value); //下面就是關於LRUHandle鏈表的實現 //可以看明的有key的hash值 //key的資料 LRUHandle* next_hash; LRUHandle* next; LRUHandle* prev; size_t charge; // TODO(opt): Only allow uint32_t? size_t key_length; uint32_t refs; uint32_t hash; // Hash of key(); used for fast sharding and comparisons char key_data[1]; // Beginning of 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去體會。
在ShardedLRUCache中我們知道插入一個key是要通過hash值決定key所在的LRUCache數組,之後把key交給數組中相應的LRUCache對象處理,這就調用了
LRUCache::Insert函數
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())); //指定新節點的資訊 //value值 e->value = value; //key,value的delete函數,可以自訂 e->deleter = deleter; e->charge = charge; //key的hash、長度等 e->key_length = key.size(); e->hash = hash; e->refs = 2; // One from LRUCache, one for the returned handle memcpy(e->key_data, key.data(), key.size()); //將新節點追加到鏈表中 LRU_Append(e); usage_ += charge; //把新鏈表加入的資訊傳遞給table,在table中登記新節點的資訊 LRUHandle* old = table_.Insert(e); if (old != NULL) { LRU_Remove(old); Unref(old); } //加入新節點後,如果超出LRUCache的設定容量,就刪除最舊的節點 //新節點都是在前端節點前 while (usage_ > capacity_ && lru_.next != &lru_) { LRUHandle* old = lru_.next; LRU_Remove(old); table_.Remove(old->key(), old->hash); Unref(old); } return reinterpret_cast<Cache::Handle*>(e);}
註:Cache::Handle是一個空結構,沒有任何成員,也並不會執行個體化,因為毫無意義。它的存在只是為了做一個指標,是LRUCache中很多函數的傳回型別。
現在整個cache的結構和實現就基本已經講完了,只剩一個存有LRUHandle節點資訊、輔助尋找的HandleTable沒有介紹,但這並不妨礙我們畫出cache的結構圖,如下:(圖片來源自網路)
Cache類是一個抽象類別,調用全域函數NewLRUCache返回一個SharedLRUCache衍生類別對象,SharedLRUCache包含一個LRUCache數組,這麼做是因為levelDB是多線程的,每個線程訪問緩衝區的時候都會將緩衝區鎖住,為了多線程訪問儘可能快速,減少鎖開銷,ShardedLRUCache內部有16個LRUCache,這樣就可以同時訪問這十六個cache區。而LRUCache本身維護了一個雙向鏈表,鏈表的節點為LRUHandle,LRUHandle放有key-value的資料。
HandleTable:
private: // The table consists of an array of buckets where each bucket is // a linked list of cache entries that hash into the bucket. uint32_t length_; //前端節點個數 uint32_t elems_; //hash表中元素個數 LRUHandle** list_; //指標鏈表
HandleTable的實現就是數組實現的hash表,數組中放置LRUHandle指標,根據key的hash值與hash表大小的餘數定位key在hash表中的位置,而leveldb使用鏈表的方式解決競爭問題。每組鏈表的節點就是LRUCache裡的節點,只不過在這裡,鏈表的後向指標是每個LRUHandle對象的next_hash成員,即leveldb是對寫進LRUCache的節點做了一次重新排列。這樣的策略是相當聰明的,只用了一個指標數組何在節點中添加一個後向指標成員就完成了協助快速尋找的hash表。
LRUHandle** FindPointer(const Slice& key, uint32_t hash) { //hash值得求餘 LRUHandle** ptr = &list_[hash & (length_ - 1)]; //利用next_hash指標便利這個鏈表,對比節點的hash值、key while (*ptr != NULL && ((*ptr)->hash != hash || key != (*ptr)->key())) { ptr = &(*ptr)->next_hash; } return ptr; }
這是一個利用handletable尋找key的函數。
插入操作:
LRUHandle* Insert(LRUHandle* h) { //插入操作 //在handletable中尋找key //沒有則增加新節點,有則取代老節點 //老節點的刪除在上層LRUCache::Insert中完成 LRUHandle** ptr = FindPointer(h->key(), h->hash); LRUHandle* old = *ptr; h->next_hash = (old == NULL ? NULL : old->next_hash); *ptr = h; //元素計數elems_更新 //元素過多,需要resize hash表,增添新的鏈表 if (old == NULL) { ++elems_; if (elems_ > length_) { // Since each cache entry is fairly large, we aim for a small // average linked list length (<= 1). Resize(); } } return old; }
hash表過大時,就要對hash表resize操作:
void Resize() { //重新選定hash表的大小,也就是鏈表的個數 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); //因為鏈表數量變了,所以依據hash值定位鏈表hash & (new_length - 1)結果變了 //原先頭結點在數組中的位置變了 uint32_t count = 0; for (uint32_t i = 0; i < length_; i++) { 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++; } } //刪除原有鏈表頭結點指標數組 //更新handletable資料 assert(elems_ == count); delete[] list_; list_ = new_list; length_ = new_length; }
如果table中的鏈表數不變,那隨著緩衝的key越來越多,每個鏈表的長度就會逐漸增加,會帶來查尋效率的底下。建立一個擁有更多元素的hash表很有必要,resize意味著原有的所有節點都要在舊的鏈表中聯絡要被打斷,建立新的聯絡。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。