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相關內容介紹完畢,還剩下一部分方法沒有介紹,大家可自行閱讀。