前面寫了兩篇文章介紹 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_checksums
和 fill_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 。我們來看看讀操作的過程:
- 獲取互斥鎖。
- 獲取本次讀操作的 Sequence Number:如果 ReadOptions 參數的 snaphot 不為空,則使用這個 snapshot 的 Sequence Number;否則,默認使用 LastSequence(LastSequence 會在每次寫操作后更新)。
- MemTable, Immutable Memtable 和 Current Version 增加引用計數,避免在讀取過程中被后臺線程進行 Compaction 時“垃圾回收”了。Version 主要用來維護 SST 文件的版本信息。
- 釋放互斥鎖 ,下面 5 和 6 兩步是沒有持有鎖的,特別是第 6 步。
- 構造 LookupKey 。
- 查找
- 獲取互斥鎖
- 更新 SST 文件的統計信息,根據統計結果決定是否調度后臺 Compaction。
- MemTable, Immutable Memtable 和 Current Version 減少引用計數。
- 釋放鎖(由析構函數完成),返回結果。
MemTable
上面分析讀流程的時候,可以發現第 6 步,從 Memtable、Immutable Memtable 和 Current Version 指向的 SST 文件查找內容是不需要持有鎖的。這樣做沒有并發讀寫的問題嗎?
簡單分析一下:引用計數保證了相關文件和內存數據結構不會被回收,而 Immutable Memtable 和 SST 文件都是只讀的,沒有并發讀寫問題。所以,只要看 MemTable 是否支持并發讀寫。
leveldb::MemTable 底層的實現是 leveldb::SkipList 。在 leveldb::SKipList 有一段注釋說明 ,簡單地說就是:
- 寫寫沖突需要外部同步。
- 讀寫沖突不需要外部同步,只要保證 SkipList 不會被垃圾回收就好。
- 這里的 SkipList 只插入,不修改和刪除 。
因此,從這段注釋可以看出,MemTable 支持一寫多讀同時并發操作。后面有機會聊到 LevelDB 的寫操作再來介紹一下 SkipList 的 Insert 操作如何實現讀寫并發不需要鎖。
LookupKey
LevelDB 通過 user_key 和 sequence 構造 leveldb::LookupKey ,用于 LevelDB 內部接口的查找。參考 LookupKey 的代碼和注釋 ,其格式為:
- klength 的類型是 varint32,是 leveldb 內部編碼的可變長度的 uint32_t,存儲 userkey + tag 的長度。表示一個 varint32 最多需要 5 個字節。
- userkey 就是一個 userkey 的 char 數組。
- tag 是使用 LittleEndian 編碼的 uint64,其組成是 7 字節的 sequence 和 1 字節的 value_type。
- 所以,一個 LookupKey 的最大長度為: 5 + userkey size + 8 = userkey size + 13。
SST 文件的查找
LevelDB 中將 SST 文件的管理實現成 leveldb::Version ,同時實現了 leveldb::VersionSet 管理多個 Version —— 因為 LevelDB 要支持 MVCC 所以可能同時存在多個版本。
查找的時候,獲取當前版本 current , 調用 leveldb::Version::Get 在 SST 上進行查找。
- 從 level0 開始一層一層查找 —— 小 level 的數據比大 level 新,所以如果先找到了的話可以直接返回。
- 在要查找的 level 收集需要查找的文件。level0 的 sst 文件比較特殊,是直接由 Immutable MemTable dump 得到的,因此,每個文件的 key 范圍可能重疊。level0 可能需要查找多個文件,其它 level 的 文件的 key 不會重疊,至多只需要讀一個文件。
- 對步驟 2 收集到的文件進行查找。具體查找邏輯由 leveldb::TableCache::Get 實現。這里面涉及一些 Cache 相關的實現,暫時略過。
- 查找過程會記錄一些統計信息:如果不止讀取一個 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 這些相關的東西沒提及。