leveldb深度剖析-存储结构(1)

本文深入剖析LevelDB源码,重点介绍其数据存储结构及压缩算法。LevelDB使用多种文件如.log、.ldb和MANIFEST-序号进行数据管理,采用7位压缩存储和SkipList实现高效内存操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从今天开始深入剖析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元数据信息元信息文件
.ldbleveldb核心数据库文件,用于存储数据.level0~level6物理磁盘表现形式从1.14版本以后不在以.sst作为后缀名,网上的很多分析都是.sst后缀名。
 CURRENT文本文件,用于指定当前使用的清单文件 
 LOCKLOCK锁文件,用于并发 
 LOGleveldb的日志文件,我们通常理解的日志信息,方便定位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进行填充。

下一篇继续介绍ldb文件格式。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值