理清leveldb基本架構圖之後,我又標出了一些比較重要(主要因爲我是小白)的信息點,如下:
[comment<>(在代碼 [db/filename.h]可以看到leveldb的幾種文件類型)
// Owned filenames have the form:
// dbname/CURRENT
// dbname/LOCK
// dbname/LOG
// dbname/LOG.old
// dbname/MANIFEST-[0-9]+
// dbname/[0-9]+.(log|sst|ldb)
我們先按照寫數據的順序對着代碼依次介紹。
LOG
寫入數據的時候,最開始會寫入到log文件中,由於是順序寫入文件,所以寫入速度很快,可以馬上返回。
log日誌的格式說明[doc/log_format.md]
-
一個Log文件由多個
Block
組成,每個Block大小爲32KB。 -
一個Block內部又有多個
Record
組成,Record分爲四種類型(還有一個留爲預分配文件[db/log_format.h]):- Full:一個Record佔滿了整個Block存儲空間。
- First:一個Block的第一個Record。
- Last:一個Block的最後一個Record。
- Middle:其餘的都是Middle類型的Record。
-
一個Record由幾部分組成:
- Header部分
- 32位長度的CRC
- 16位長度的Length:存儲數據部分長度。
- 8位長度的Type:存儲Record類型,就是上面說的四種類型。
1.1 寫log
寫過程舉個例子,我們現在想把這些數據寫入:
A: 長度 1000
B: 長度 97270
C: 長度 8000
我們先裝第一個block。可以看到A數據很小,所以用FULL
就可以裝下,到這時第一個record還剩31761B
的空間。
下面繼續裝B,但是它好大,要把它切分再裝。B的第一部分要接着第一個Record去裝,所以在這裏它的RecordType
是First
,裝了31761B
的數據。這時候第一個block已經滿了,再來一個block,這個block刨除record的hearder、crc
等部分,還可以裝32761B
的數據,當然這還是不夠B裝的,沒關係,我們接着開一個block裝。而這時,對於B的第二個部分的RecordType
就是Second
了。這時B還剩餘32655B
的數據,一個block是裝得下的,還剩了6B
的空間,我們留出來做trailer
致鬱C,和A一樣,都是FULL record,落在第四個block中。
上述過程可以用一張圖直觀表示:
總結一下,log有多個固定大小的block組成 ,block又由record組成,record是連續的,數據可能會被拆到不同的record上。
寫操作類Writer
中的接口函數是AddRecord
Status AddRecord(const Slice& slice);
簡單看一下這個函數:
status:狀態
block_offset_ : 當前block用(偏移)到哪裏了
leftover : 當前block還剩多少
left:待寫入數據
kBlockSize:32(32768,Bytes)
kHeaderSize:7(4+2+1,Bytes)
type:即RecordType
while(status_is_ok && left>0) {
if (leftover < kHeaderSize) {
// 用0填充
}
// 根據left、block_offset_,更新RecordType
// 真正寫入過程由EmitPhysicalRecord完成,包括生成一個record頭部,追加數據
// 更新status
// 更新left
}
1.2 讀log
讀操作類Writer
中的接口函數是ReadRecord
bool ReadRecord(Slice* record, std::string* scratch);
// 真正讀入過程由ReadPhysicalRecord實現:從文件中每次讀取一個Block,Read內部會做偏移,保證按順序讀取,並判斷各種badrecord的情況
// 根據recordtype,向switch指向的內存中追加數據
switch(recordtype) {
case Full:
case First:
case Middle:
case Last:
}
Slice是一個結構體,其中只有兩個成員,一個指向外存的指針,一個是大小。
代碼實現用了switch...case
真是棒呆。具體的代碼邏輯我加了一些註釋,可以具體瞭解一下。