从今天开始深入剖析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进行填充。