當向leveldb寫入資料時,首先是将資料寫入leveldb的Memtable(Memtable可能轉化為IMMemtable)中,Memtable是存儲在記憶體中的。隻有經過compaction操作後,才會将記憶體中的資料寫入到磁盤中的sstable中。
當要讀資料時,首先在Memtable中查找,若沒有找到,則在sstable中繼續查找。而sstable是存儲在磁盤中的,這樣就需要進行多次磁盤操作,速度會非常慢。為了加快查找速度,leveldb在采用了Cache的方式,盡最大可能減少随機讀操作。
cache分為Table Cache和 Block Cache兩種,其中Table Cache中緩存的是sstable的索引資料,Block Cache緩存的是Block資料,Block Cache是可選的,即可以在配置中來選擇是否打開這個功能。
當要進行Compaction操作調用CompactMemTable()時,會調用WriteLevel0Table(),此時則會建立一個Meta File,并儲存在Table Cache中,然後可通過Table Cache進行讀取。
leveldb中的Cache主要用到了雙向連結清單、哈希表和LRU(least recently used)思想。
1、LRUHandle
LRUHandle表示了Cache中的每一個元素,通過指針形成一個雙向循環連結清單:
struct LRUHandle {
void* value;//cache存儲的資料
void (*deleter)(const Slice&, void* value);//是将資料從Cache中清除時執行的函數
LRUHandle* next_hash;//解決hash碰撞,指向下一個hash值相同的節點
LRUHandle* next;//next和prev構成雙向循環連結清單
LRUHandle* prev;
size_t charge; // 所占記憶體大小
size_t key_length;
uint32_t refs;
uint32_t hash; // Hash of key(); used for fast sharding and comparisons
char key_data[]; // Beginning of key
Slice key() const {
if (next == this) {
return *(reinterpret_cast<Slice*>(value));
} else {
return Slice(key_data, key_length);
}
}
};
在Table Cache中,Cache的key值是SSTable的檔案名稱,Value部分包含兩部分,一個是指向磁盤打開的SSTable檔案的檔案指針,這是為了友善讀取内容;另外一個是指向記憶體中這個SSTable檔案對應的Table結構指針。這樣就将不同的sstable檔案像cache一樣進行管理。
leveldb通過LRUHandle 結構将hash值相同的所有元素串聯成一個雙向循環連結清單,通過指針next_hash來解決hash 碰撞。
2、HandleTable
leveldb通過HandleTable維護一個哈希表:
class HandleTable {
public:
HandleTable() : length_(0), elems_(0), list_(NULL) { Resize(); }
~HandleTable() { delete[] list_; }
LRUHandle* Lookup(const Slice& key, uint32_t hash);
LRUHandle* Insert(LRUHandle* h);
LRUHandle* Remove(const Slice& key, uint32_t hash);
private:
uint32_t length_;
uint32_t elems_;
LRUHandle** list_;
LRUHandle** FindPointer(const Slice& key, uint32_t hash) ;
void Resize() ;
};
HandleTable隻有3個成員變量: uint32_t length_; //hash表長,相當于二維數組的元素個數
uint32_t elems_; //hash表中元素總個數
LRUHandle** list_; //hash表頭,相當于一個雙向循環連結清單數組,其中每一個元素指向一個循環連結清單
初始時,length_=elems_=0, list_=NULL;此時hash表為空
然後調用resize()為hash表配置設定空間,初始時配置設定4個元素,當空間不足時,則成倍增加。此時hash表的結構如下:
3、LRUCache
LRUCache顧名思義是指一個緩存,同時它用到了LRU的思想
class LRUCache {
public:
LRUCache();
~LRUCache();
void SetCapacity(size_t capacity) { capacity_ = capacity; }
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);
private:
void LRU_Remove(LRUHandle* e);
void LRU_Append(LRUHandle* e);
void Unref(LRUHandle* e);
// Initialized before use.
size_t capacity_;
// mutex_ protects the following state.
port::Mutex mutex_;
size_t usage_;
LRUHandle lru_;
HandleTable table_;
};
LRUCache維護了一個雙向循環連結清單lru_和一個hash表table,當要插入一個元素時,首先将其插入到連結清單lru的尾部,然後根據hash值将其插入到hash表中。
當hash表中已存在hash值與要插入元素的hash值相同的元素時,将原有元素從連結清單中移除,這樣就可以保證最近使用的元素在連結清單的最尾部,這也意味着最近最少使用的元素在連結清單的頭部,這樣即可實作LRU的思想。
capacity_:目前Cache的最大容量
usage_:目前Cache已使用的記憶體大小,當超過最大容量時,從連結清單的頭部開始移除元素
mutex_:保證每個Cache都是線程安全的
4、Cache和ShardedLRUCache
Class Cache采用虛函數定義了Cache的接口:
class Cache {
public:
Cache() { }
virtual ~Cache();
struct Handle { };//handle corresponds to a mapping
virtual Handle* Insert(const Slice& key, void* value, size_t charge,
void (*deleter)(const Slice& key, void* value)) = ;
virtual Handle* Lookup(const Slice& key) = ;
virtual void Release(Handle* handle) = ;
virtual void* Value(Handle* handle) = ;//handle returned by Lookup()
virtual void Erase(const Slice& key) = ;
virtual uint64_t NewId() = ;
private:
void LRU_Remove(Handle* e);
void LRU_Append(Handle* e);
void Unref(Handle* e);
struct Rep;
Rep* rep_;
};
具體實作的ShardedLRUCache繼承自Cache,實作了相應的功能:
class ShardedLRUCache : public Cache {
private:
LRUCache shard_[kNumShards]; //kNumShards = 2^4 = 16
port::Mutex id_mutex_;
uint64_t last_id_;
public:
virtual Handle* Insert(const Slice& key, void* value, size_t charge,
void (*deleter)(const Slice& key, void* value));
virtual Handle* Lookup(const Slice& key);
virtual void Release(Handle* handle) ;
virtual void Erase(const Slice& key) ;
virtual void* Value(Handle* handle) ;
};
由于每一個LRUCache是線程安全的,為了多線程通路,盡可能快速,減少鎖開銷,ShardedLRUCache内部将所有Cache根據hash值的高4位分為16份,即有16個LRUCache分片。
查找Key時首先計算key屬于哪一個分片,分片的計算方法是取32位hash值的高4位,找到對應的LRUCache配置設定,然後在相應的LRUCache中進行查找,這樣就大大減少了多線程的通路鎖的開銷。以Insert為例:
virtual Handle* Insert(const Slice& key, void* value, size_t charge,
void (*deleter)(const Slice& key, void* value)) {
const uint32_t hash = HashSlice(key);//首先得到hash值的高4位,用于找到相應的分片
return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);//然後在對應的LRUCache中進行相應的插入、移除等操作
}
由上可知,ShardedLRUCache最終都是調用LRUCache的相應操作來實作的,是以下面将詳細介紹LRUCache中的相關操作
5、LRUCache的具體操作
假設此時剛剛完成LRUCache的初始化和Resize()
插入元素:
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)- + key.size()));
e->value = value; //首先填充結構體LRUHandle
e->deleter = deleter;
e->charge = charge;
e->key_length = key.size();
e->hash = hash;
e->refs = ; // One from LRUCache, one for the returned handle
memcpy(e->key_data, key.data(), key.size());
LRU_Append(e); //然後将元素加入到雙向循環連結清單中
usage_ += charge;
LRUHandle* old = table_.Insert(e); //将元素加入到hash表中
if (old != NULL) {//當hash表中已存在hash值相同的元素時,将原有的元素移除
LRU_Remove(old);
Unref(old);
}
//當所有占用空間超過最大容量時,則從雙向連結清單的表頭開始移除元素
//由于表頭元素是最早加入的,是以是實作了LRU思想
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);
}
1) 調用LRU_Append(e) 将元素加入到雙向循環連結清單中:
雙向連結清單的插入操作示意圖如下:
當插入節點D時:
void LRUCache::LRU_Append(LRUHandle* e) {
// Make "e" newest entry by inserting just before lru_
e->next = &lru_;//D->next 指向表頭
e->prev = lru_.prev; //D->prev=A->prev=C
e->prev->next = e; //D->prev->next = C->next = D
e->next->prev = e; //D->next->prev = A->prev = D
}
這樣就将一個節點插入到雙向連結清單的尾部了
2) 調用 LRUHandle* old = table_.Insert(e); 向hash表中插入一個元素:
LRUHandle* Insert(LRUHandle* h) {
LRUHandle** ptr = FindPointer(h->key(), h->hash);//首先在hash表中進行查找
LRUHandle* old = *ptr;
h->next_hash = (old == NULL ? NULL : old->next_hash);//當old不為NULL時,表示hash表中已存在一個與要插入的元素的hash值完全相同的元素,這樣隻是替換元素,不會改變元素個數
*ptr = h;
if (old == NULL) {//old為空表示插入一個新元素,要增加元素個數并調整hash表大小
++elems_;
if (elems_ > length_) {
Resize();
}
}
return old;
}
當要插入一個元素時,首先進行查找:
LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
LRUHandle** ptr = &list_[hash & (length_ - )];
while (*ptr != NULL &&
((*ptr)->hash != hash || key != (*ptr)->key())) {
ptr = &(*ptr)->next_hash;
}
return ptr;
}
假設首先要插入元素的hash值為1:
此時length_ = 4,則 [ hash & (length_ - 1) ]=[01 & 11]= [1],即ptr= &list_[1],此時*ptr = NULL
則傳回ptr并完成插入操作,此時hash表為:
當要繼續插入元素,且元素的hash值為5時:
此時length_ = 4,則 [ hash & (length_ - 1) ]=[101 & 11]= [1],ptr= &list_[1],此時*ptr = e(1),繼續向後查找,ptr=&(*ptr)->next_hash; 最終*ptr = NULL ,則傳回ptr并完成插入操作,此時hash表為:
插入操作的基本操作為:首先構造LRUHandle結構體,然後将其插入到雙向連結清單中,然後根據hash值将其插入到hash表中,并調整hash表的大小,最後根據記憶體大小,移除雙向連結清單表頭後第一個元素,來實作LRU算法。
其它操作如Lookup()、Erase()、Release()等,都是先根據hash值的高四位來找到對應的LRUCache,然後在相應的hash表中根據hash值找到相應元素,最終來對找到的元素執行相關操作。
6、總結
1)一個SharedLRUCache根據hash值的高4位,将所有的Cache分為16份,每一份為一個LRUCache。
2)LRUCache用來維護所有元素(LRUHandle),通過鎖mutex來保證線程安全,同時記錄目前
Cache的最大容量和已用記憶體。
3)每一個LRUCache中有一個循環連結清單(LRUHandle)和一個hash表(HandleTable),通過循環鍊
表實作LRU思想,通過hash表來加快查找速度。
假定設定的最大容量為4,即capacity_=4,然後依次向其中插入元素1、5、2、4、9,簡單的示意圖如下:
當插入元素1,5, 2, 4後,usage=4。每個元素在hash表中的位置為 list [ hash&length_-1 ] ,由此可知 ,1和5,2和4有着相同的hash值,分别對應list[1],list[2]
此時得到如下結果(為簡化,與循環連結清單頭部相連的部分指針線未畫出):
此時已達到最大容量,假設要繼續插入一個元素9(對應list[1]),插入9後,usage>capacity,則要從連結清單頭部開始移除元素,即元素1會被移除,得到結果如下: