理清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
真是棒呆。具体的代码逻辑我加了一些注释,可以具体了解一下。