從今天開始深入剖析leveldb源碼,在工作中也有用到leveldb(雖然沒出過問題),但是從個人興趣來說還是比較喜歡這款高效、簡單的數據庫。其實我們一直在使用leveldb,只是大家可能沒有發現。例如:Chrome(谷歌瀏覽器)底層存儲就是使用的leveldb。我是基於leveldb-v.1.20穩定版本進行分析。
爲什麼我第一篇關於leveldb的博客不是介紹leveldb的簡介、性能等基礎東西而是直接介紹數據結構,主要有兩個原因:
1)、關於leveldb簡介、性能分析現在網上有很多,不想重複造輪子了,所以我就不介紹了(也介紹不好)。
2)、通過我閱讀leveldb源碼以及網絡上其他人的博客,我發現了沒有一篇博客是介紹leveldb存儲結構,或者是說沒有圖形化介紹存儲結構。因爲leveldb是數據庫,如果能夠很清晰的描述出leveldb各種存儲結構是如何存儲的,對於閱讀代碼起到事半功倍的效果。
對於leveldb不是很瞭解的讀者,可以先去別的博客瞭解一下相關概念,然後再閱讀本博客是比較好的。
一、存儲文件
使用leveldb進行數據存儲,會生成以下幾個文件:
文件名 | 作用 | 備註 |
序號.log (000003.log) | 操作日誌,主要記錄添加、刪除流程 | 例如:寫入一條記錄,會寫到.log文件中,然後在寫入到Memtable中,保證異常重啓數據不丟失。 |
MANIFEST-序號 (MANIFEST-000002) |
清單文件,用於管理leveldb元數據信息 | 元信息文件 |
.ldb | leveldb核心數據庫文件,用於存儲數據.level0~level6物理磁盤表現形式 | 從1.14版本以後不在以.sst作爲後綴名,網上的很多分析都是.sst後綴名。 |
CURRENT | 文本文件,用於指定當前使用的清單文件 | |
LOCK | LOCK鎖文件,用於併發 | |
LOG | leveldb的日誌文件,我們通常理解的日誌信息,方便定位bug |
各個文件具體作用,後面小節會進行詳細介紹說明。
二、數字7位壓縮存儲(非常經典)
著名的RPC框架Protobuf也使用了該算法.
關於該算法是網上有很多介紹,主體思想是:用7bit來表是數據,最高8bit位表示數據是否完整(1--未完整,0--完整)。
以int i = 300(佔用4字節)來舉例說明,
300的二進制表示位: 00000000 00000000 00000001 00101100
通過壓縮後只需要2字節: 10101100 00000010
其中紅色(0/1)代表數據是否完整,藍色對應原來數據,編碼之後字節順序是反的。對於負數的編碼,需要先進行反碼操作在對其進行編碼,由於本片並不是介紹該算法的,大家可自行查詢相關文章。
後面用varint32,varint64表示uint32_t,uint64_t進行壓縮後數據類型。
三、MemTable
leveldb是key-value型數據,它在內存中組織形式以MemTable(類)呈現,而底層存儲結構是通過SkipList(跳錶)進行關聯各個key-value pair。下面介紹SkipList節點存儲結構,具體格式如下:
說明:
1)橘色internal_key_length,value_length是可變長度,是對uint32_t進行7比特位壓縮。
2)leveldb爲了管理數據,在內部抽象出InternalKey對象。 internal_key由三部分組成: 用戶輸入key值,序列號,操作類型。
序號:在leveldb源碼中定義爲uint64_t,但是實際可表示範圍是低56bit(高8bit不可用)。
類型:在leveldb源碼中定義爲枚舉類型,kTypeDeletion(0x0)、kTypeValue(0x01)
3)value_length代表用戶輸入value值長度。
以上存儲結構作爲SkipList節點中實際內容,但是需要提醒節點數據結構中並沒有保存完整內容,而是保存了指針。數據內容保存在內存池中(MemTable::arena_)。當Memtable佔用空間爲4M(默認值)則將其進行壓縮存儲到level0。
四、.log和MANIFEST文件存儲格式
這兩個文件按照Block存儲,一個Block是32K,一個Block存儲多個記錄(Record),每個Record存儲格式如下:
字段名稱 | 含義 |
CRC | 根據Type不同,設置不同的CRC |
Length | 代表buffer的長度 |
Type | 當一個Record長度大於一個Block大小(32k),則需要進行分片處理,這裏Type就是用於標記分片,具體取值如下所示。 |
buffer | 具體存儲內容,字節數組 |
enum RecordType {
// Zero is reserved for preallocated files
kZeroType = 0,
kFullType = 1, //表示當前Record沒有超過32k
// For fragments 表示當前Record超過了32k需要分片
kFirstType = 2, // 第一個分片
kMiddleType = 3, // 中間分片
kLastType = 4 // 最後一個分片
};
4.1、MANIFEST文件(清單)
上圖中的buffer是7bit壓縮後的VersionEdit對象,具體實現方法爲:VersionEdit::EncodeTo,壓縮後的數據格式如下:
上面圖片說明:
1)雖然是按行分開的,實際在文件系統中是連續存儲的。
2)Varint32代表對int32類型進行7bit壓縮存儲。
3)藍色內容爲類型,可參enmu Tag定義,每一個類型的數據有可能是不存在的。
深橙色爲具體內容。
// Tag numbers for serialized VersionEdit. These numbers are written to
// disk and should not be changed.
enum Tag {
kComparator = 1,
kLogNumber = 2,
kNextFileNumber = 3,
kLastSequence = 4,
kCompactPointer = 5,
kDeletedFile = 6,
kNewFile = 7,
// 8 was used for large value refs
kPrevLogNumber = 9
};
4.2、.log文件
一個log文件是以多個block組成,一個block大小是32KB,一個block又由多個Record組成,其Record結構如下:
舉例說明:
各個字段說明:
字段 | 說明 |
crc | 數據校驗和 |
length | 有效數據長度,即Record Data部分,不包含Record Header部分 |
RecordType | 當前Record類型,具體參考下表。 |
SequenceNumber | 當前Record中第一個key-value編號,最後一個key-value編號是通過SequenceNumber+count計算得知 |
count | 當前Record中包含多少個key-value對 |
type | 表示當前key-value操作類型,只有添加和刪除,具體參考下表 |
RecordType取值 | 說明 |
kZeroType = 0 | 保留字段 |
kFullType = 1 | 表示當block可以滿足此次數據存儲 |
kFirstType = 2 | 表示當前block剩餘空間不足,需要跨越多個block,存儲數據。這是一個分片 |
kMiddleType = 3 | 表示當前block剩餘空間不足,需要跨越多個block,存儲數據。這是中間分片 |
kLastType = 4 | 表示當前block剩餘空間不足,需要跨越多個block,存儲數據。這是最後一個分片,只有遇到這個類型才認爲是一個Record結束標誌。 |
type | 說明 |
kTypeValue | 向leveldb添加key-value |
kTypeDeletion | 從leveldb中刪除key-value,當是此類型時,只有key值,不需要value值。 |
特別說明:當一個block剩餘空間小於7字節,則不再進行數據存儲,而是從新啓用一個新的block,但是爲了數據對齊,需要將剩餘空間補足32KB,以0進行填充。