定义
sst 文件是用来持久化数据库数据的文件,LevelDB 使用的就是sst文件, 其充分考虑了持久化,读写性能和存储空间的权重
设计
设计模式
接口设计
创建文件
sst 文件由原始数据经过序列化后存入磁盘,因此需要进行编码和解码,那么采用策略模式,创建编码器进行编码
检索文件
首先考虑到仅在 sst 文件中进行读取,有多种读取方式,那么可以采用迭代器模式读取数据
初始化
当第一次启动 DB 时,需要恢复数据索引,所以还需要进行数据库的初始化
编码
基本结构
一段编码需要 类型,长度,数据本身,当我们把长度和数据合并,就会变成变长编码(例如
c
h
a
r
char
char 数组用结尾是
0
0
0 表示字符串结束)
我们考虑如何遍历一段数据,记录一段数据的总长度,再按顺序记录组成数据的每个元素的长度,就可以用一段二进制编码还原出初始数据,记录长度的数组我们暂时称为
o
f
f
s
e
t
offset
offset
sst 文件的编码
sst 为了提高查询新能,在编码中加入了索引,为了更清晰的描述数据,引入了元数据段
sst 文件编码分为三部分,元数据,索引段,数据段
元数据
更复杂的描述一段数据的类型,支持事物,时间戳,是否删除等
其数组组成由 数据类型,索引长度,数据长度 组成
索引段
索引段由 布隆过滤器 和 offset 组成
布隆过滤器用来快速检索当前 sst 文件中是否存在目标数据,offset 用来二分查找 数据段中的数据
数据段
数据段存储 kv 对
数据格式
sst 将 数据分成 data 区域 和 index 区域依次存入
data 区域
data 区域所有的 key 是递增的
考虑到较多数据下,data区域会变的非常大,主存无法存下一个sst文件,那么将data分为许多block,每个block有各自的布隆过滤器,对所有的block抽象出索引,这样查询时只需要读入索引,由索引找到对应的block
data区域由许多 block 组成
sst编码设计
KV对
存储了 kv 的 size,这样拿到一块 kv 就可以解析出 key 和 value,无需在外部设立索引
kv 的 size一般较小,为了节省空间使用了 binary 的变长编码
type KVStruct struct {
KeySize uint64
ValueSize uint64
Key []byte
Value *Utils.ValueStruct
}
block
由上到下存储,offset 存储每个 kv 结构的地址
由于需要在 block 上二分查找,我们将 kv 的地址拿出来做成索引
type block struct {
KV []KVStruct
kvOffset []uint64
count uint64
}
index
每一个 index 存储了对应 block 的信息,前面 block 的设计为知道开头位置即可解码,现在我们只知道文件的头和尾,为了二分时的便捷,将 block 设计为了在文件开头顺序排列,且block 内的信息可以顺序解码,index紧跟在block后面,index内的信息可以顺序解码
type index struct { //一个index 描述一个block
blockOffset uint64
maxVersion uint64
KeyCount uint64
filter *Utils.Bloom
}
缺点
没有设计统一的编码方式,代码耦合度高(当时已经写了一大半不想改了)
sst
现在我们希望只读取索引,那么我们就需要每个索引的 offset,因为每个索引的大小不同(内置布隆过滤器),那么我们可以将索引的offset固定大小并放到文件末尾
type SSTable struct { // block 在前, index 在后,目的是可以在索引上二分,然后直接读取一个block
Blocks []block
Ind []index
IndOffset []uint64
blockCount uint64
}
内存映射
在 Linux 中,调用系统的 IO 接口时,系统会先将数据放入磁盘缓存中,积累一定程度后再放入磁盘中,也就是将数据从用户态的内存读到内核态的内存再进行磁盘读写,其目的是解决大量小数据存储问题
但是我们已经精心设计过了数据的结构,希望直接存入磁盘
mmap
Linux 针对上述情况提供了 mmap
,实现物理地址和虚拟地址的一一映射关系,调用 mmap 同步函数后相当于直接将数据存入磁盘