leveldb源碼剖析---緩存系統

通過前面的分析可以知道,leveldb爲了提高寫的性能,犧牲了部分的讀性能。最差的情況可能需要遍歷各個level中的每個文件。爲了緩解讀性能,leveldb引入了緩存機制,當然,版本信息中包含各個level的文件元信息在一定程度上也可以提高讀性能。


leveldb提供的緩存系統的底層數據結構是一個開鏈哈希

class ShardedLRUCache : public Cache :

    LRUCache shard_[kNumShards];
  • 1
  • 2
  • 3

它是一個數組,其中數組中的每個元素包含兩個鏈表,和一個用於加速鏈表元素查找的哈希表 table_。

class LRUCache:
     ....
    LRUHandle lru_;
    LRUHandle in_use_; 
    HandleTable table_;
    ....
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

鏈表中的每個元素就表示一個緩存單元–LRUHandle

LRUHandle :
  void* value;
  char key_data[1];
  ....
  LRUHandle* next_hash; //這個指針是用來將LRUHandle鏈到hashtable中的
  LRUHandle* next;
  LRUHandle* prev;
  ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這裏我們只關注這一部分元素。首先是鏈表元素的主要內容,我們可以看到,每個鏈表元素都是一個key-value形式的數據

1. 其中key 爲對應sstable的file_number,通過它可以打開磁盤上的其所指向的sstable文件。
2. value爲TableAndFile,裏面包含這個sstable文件的許多元信息,包括 index_block,metadataindex_block信息等。

這裏需要提一下,前面在分析版本控制時,可以看到每個版本都會包含所有level的各個文件的元信息,需要注意的是,版本信息裏裏面的元信息爲FileMetaData,和它相比,TableAndFile包含有文件的更多信息。


這裏我們簡單理一下FileMetaData和TableAndFile是怎麼互相幫助提高讀性能的:
當用戶調用leveldb的get函數,期望獲得對應key的value時
1. leveldb先從版本信息中檢查各個level中的各個文件的FileMetaData,查看這個key是否有可能存在於對應的sstable中
2. 如果有可能存在在這個sstable中,則根據這個sstable文件的FileMetaData提供的file_number在緩存中找到對應的TableAndFile;當然,如果緩存中沒有這個sstable的TableAndFile,則從磁盤中將其讀進來。
3. 進一步根據TableAndFile中的index_block,metadata_block等信息確定該key是否在這個sstable中。

具體的細節可以從

Status DBImpl::Get(const ReadOptions& options,
                   const Slice& key,
                   std::string* value)
  • 1
  • 2
  • 3

函數的實現中找到


下面我們繼續分析緩存系統:

除了存儲緩存具體數據的元素之外,緩存鏈表中還包括鏈表組織的數據
next,prev表示這是一個雙向鏈表。它將緩存系統中的所有sstable緩存元數據組織了起來。前面講到,爲了方便緩存查找,leveldb使用了開鏈哈希。但是開鏈哈希也有可能存在問題,比如如果開鏈數組中單個元素指向的鏈表過長,則對這個鏈表的線性掃描還是會非常耗費時間。爲了解決這個問題,leveldb在每個鏈表上再建立了一層哈希。這就是結構中next_hash的作用。因此leveldb利用了二維哈希來緩解了對於開鏈哈希可能存在的單個鏈表過長的問題。

如果我們深入看HandleTable的代碼,就會發現,它也是一個開鏈哈希。通過next_hash將元素鏈在這個哈希表中。

最後,緩存系統中有兩條鏈表 :in_lru_和in_use_。

作用是顯而易見的。我們知道鏈表中的每個元素(LRUHandle)用

 uint32_t refs; 
  • 1

來做爲引用計數標記該元素目前正在被多少個用戶使用,顯然如果refs>0,這個元素是不能從緩存中被刪除的。這裏需要注意的是,緩存系統本身也是這個元素的一個用戶,因此從 Cache::Handle* LRUCache::Insert函數中可以看到,每個插入新插入的元素的refs都爲2,表示它有兩個用戶,分別是
1. cache系統本身
2. 調用insert的用戶

那很自然的一個問題是這個元素什麼時候才被刪除呢?自然是它的用戶數爲0的時候。當一個元素的引用計數爲1時,說明它只有cache系統一個用戶,此時,將它移動到lru_鏈表中,在這個鏈表中的元素隨時可以被刪除掉,因爲沒有上層用戶對它使用了。它的真正刪除發生在緩存系統的容量滿載時,判斷滿載自然也是在 LRUCache::Insert函數中。當元素只cache一個用戶時,則放在lru_鏈表中,當緩存滿載時,從lru_鏈表中刪除。如果有上層用戶在使用這個元素,則將它放在in_use_鏈表中。緩存系統的負載是使用

 size_t usage_;
  • 1

來記錄的。緩存系統的容量是用

 size_t capacity_;
  • 1

記錄的。

至此leveldb的緩存系統的主要部分就講完了。


總結

leveldb引入了緩存系統來改善它的讀性能。緩存系統本質上是將最近讀過的sstable文件的元信息(TableAndFile)放在內存中,以避免每次讀取數據時都要直接讀盤。在讀取某個給定key的value時,leveldb首先通過版本信息找到可能包含該key的文件的file_number,然後通過這個file_number找到緩存系統中保存的更加完整的文件元信息。如果緩存系統中沒有這個file_number對應的元素,則需要讀盤,並在內存中創建對應這個sstable文件的TableAndFile,並插入緩存中。

緩存系統是通過二維開鏈哈希將緩存中所有的sstable的元信息組織起來的。並利用引用計數設計了二層的緩存,將暫時不用的元素也依舊緩存在系統中,只有負載過高時才真正刪除。當然爲了避免同步問題,對緩存元素的鏈表操作必須在臨界區內。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章