LevelDB:讀操作

前面寫了兩篇文章介紹 LevelDB 的整體架構接口使用。這篇文章,我們從代碼的角度看看 LevelDB 的設計與實現,先從讀操作開始。

LevelDB 的版本更新不是很頻繁,整體變化不大。本文的源代碼參考和索引的版本是 LevelDB v1.20

LevelDB 的目錄結構很簡單,就不用介紹了,直接進入正題吧。

leveldb::DB

LevelDB 暴露給外部的操作接口都封裝在 leveldb::DB 這個抽象類里,具體實現是 leveldb::DBImpl 。使用時,leveldb::DB 用于引用一個 LevelDB 實例。一個 LevelDB 實例可以簡單認為是一個支持并發讀寫和持久化的 map。

LevelDB 暴露給外部的操作接口都很簡單,具體可以根據上面提供的索引鏈接看看代碼和注釋。

讀操作

leveldb::DB::Get 根據 Key 獲取 Value,先看看函數的原型:

virtual Status Get(const ReadOptions& options, const Slice& key, std::string* value) = 0;

leveldb::ReadOptions 是讀操作的一些參數。verify_checksumsfill_cache 是兩個偏優化的參數,重點是 snapshot 參數,表示本次讀操作要從哪個 Snapshot 讀取。snapshot 默認是 NULL,此時 LevelDB 會從當前 Snapshot 讀取。

講到 Snapshot,我們順便來看看 leveldb::Snapshot 的實現。leveldb::Snapshot是個空殼,具體實現是在 leveldb::SnapshotImpl ,也相當簡單,和 Snapshot 相關的變量只有一個 number_ (SequenceNumber) —— 不難看出 LevelDB 是通過維護一個 Sequence Number 來實現快照功能。

LevelDB 單個 Key 的讀取操作的具體實現是 leveldb::DBImpl::Get 。我們來看看讀操作的過程:

  1. 獲取互斥鎖
  2. 獲取本次讀操作的 Sequence Number:如果 ReadOptions 參數的 snaphot 不為空,則使用這個 snapshot 的 Sequence Number;否則,默認使用 LastSequence(LastSequence 會在每次寫操作后更新)。
  3. MemTable, Immutable Memtable 和 Current Version 增加引用計數,避免在讀取過程中被后臺線程進行 Compaction 時“垃圾回收”了。Version 主要用來維護 SST 文件的版本信息。
  4. 釋放互斥鎖下面 5 和 6 兩步是沒有持有鎖的,特別是第 6 步
  5. 構造 LookupKey
  6. 查找
    1. 從 MemTable 查找
    2. 從 Immutable Memtable 查找
    3. 從 SST 文件查找
  7. 獲取互斥鎖
  8. 更新 SST 文件的統計信息,根據統計結果決定是否調度后臺 Compaction
  9. MemTable, Immutable Memtable 和 Current Version 減少引用計數
  10. 釋放鎖(由析構函數完成),返回結果

MemTable

上面分析讀流程的時候,可以發現第 6 步,從 Memtable、Immutable Memtable 和 Current Version 指向的 SST 文件查找內容是不需要持有鎖的。這樣做沒有并發讀寫的問題嗎?

簡單分析一下:引用計數保證了相關文件和內存數據結構不會被回收,而 Immutable Memtable 和 SST 文件都是只讀的,沒有并發讀寫問題。所以,只要看 MemTable 是否支持并發讀寫

leveldb::MemTable 底層的實現是 leveldb::SkipList 。在 leveldb::SKipList 有一段注釋說明 ,簡單地說就是:

  1. 寫寫沖突需要外部同步。
  2. 讀寫沖突不需要外部同步,只要保證 SkipList 不會被垃圾回收就好。
  3. 這里的 SkipList 只插入,不修改和刪除

因此,從這段注釋可以看出,MemTable 支持一寫多讀同時并發操作。后面有機會聊到 LevelDB 的寫操作再來介紹一下 SkipList 的 Insert 操作如何實現讀寫并發不需要鎖。

LookupKey

LevelDB 通過 user_key 和 sequence 構造 leveldb::LookupKey ,用于 LevelDB 內部接口的查找。參考 LookupKey 的代碼和注釋 ,其格式為:

LookupKey.png
  1. klength 的類型是 varint32,是 leveldb 內部編碼的可變長度的 uint32_t,存儲 userkey + tag 的長度表示一個 varint32 最多需要 5 個字節
  2. userkey 就是一個 userkey 的 char 數組。
  3. tag 是使用 LittleEndian 編碼的 uint64,其組成是 7 字節的 sequence 和 1 字節的 value_type。
  4. 所以,一個 LookupKey 的最大長度為: 5 + userkey size + 8 = userkey size + 13

SST 文件的查找

LevelDB 中將 SST 文件的管理實現成 leveldb::Version ,同時實現了 leveldb::VersionSet 管理多個 Version —— 因為 LevelDB 要支持 MVCC 所以可能同時存在多個版本。

查找的時候,獲取當前版本 current , 調用 leveldb::Version::Get 在 SST 上進行查找。

  1. 從 level0 開始一層一層查找 —— 小 level 的數據比大 level 新,所以如果先找到了的話可以直接返回。
  2. 在要查找的 level 收集需要查找的文件。level0 的 sst 文件比較特殊,是直接由 Immutable MemTable dump 得到的,因此,每個文件的 key 范圍可能重疊。level0 可能需要查找多個文件,其它 level 的 文件的 key 不會重疊,至多只需要讀一個文件。
  3. 對步驟 2 收集到的文件進行查找。具體查找邏輯由 leveldb::TableCache::Get 實現。這里面涉及一些 Cache 相關的實現,暫時略過。
  4. 查找過程會記錄一些統計信息:如果不止讀取一個 sst 文件,則記錄最后讀取的是哪個 level 的哪個文件。

讀觸發的 Compaction

讀取結束后,如果不止讀取一個 sst 文件,則更新統計信息,決定是否觸發 Compaction。更新統計信息時,直接將記錄的文件的 leveldb::FileMetaData 的 allowed_seeks 減一,當 allowed_seeks <= 0時,表示讀取效率很低,需要執行 Compaction,減少這條路徑上的文件數量。

調用 MaybeScheduleCompaction 嘗試調度后臺線程的 Compaction。

小結

這里只是簡單介紹了 LevelDB 的讀操作的大概情況。實際上,LevelDB 的讀操作涉及很多東西,如:寫操作相關的并發讀寫、Sequence Number 等;Compaction 相關的 Version、VersionSet等;讀操作還有可能觸發 Compaction;還有 Table Cache、Block Cache 這些相關的東西沒提及。

參考文檔

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 版本控制或元信息管理,是LevelDB中比較重要的內容。本文首先介紹其在整個LevelDB中不可替代的作用;之后從...
    CatKang閱讀 5,533評論 12 12
  • LevelDB是Google傳奇工程師Jeff Dean和Sanjay Ghemawat開源的KV存儲引擎,無論從...
    CatKang閱讀 4,859評論 5 25
  • 通過之前對LevelDB的整體流程,數據存儲以及元信息管理的介紹,我們已經基本完整的了解了LevelDB。接下來兩...
    CatKang閱讀 3,733評論 0 5
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 作為一個存儲引擎,數據存儲自然是LevelDB重中之重的需求。我們已經在庖丁解LevelDB之概覽中介紹了Leve...
    CatKang閱讀 4,695評論 2 11