本文将从 下层的数据编码 到 上层的 kv 数据读写接口实现 完整介绍如何实现一个最简化的 kv 存储引擎,适合 Bitcask 存储模型和 Rust 语言的入门。本文的完整代码已开源在:GitHub - Morgan279/miniDB: A mini kv database demo that using simplified bitcask storage model with rust implementation.
功能和架构设计
如上图所示,本文的存储引擎支持 Get, Set, Remove 这三种最基础的命令。架构上可以分为 KV Store 和 Storage 两个部分,其中 KV Store 负责与客户端交互,接收外部命令和返回操作结果;Storage 负责持久化经过编码之后的数据。图里面的 Entry 是指数据的二进制编码,即每一份数据都会先封装成 Entry 然后编码成二进制之后再存入 Storage(实际的物理存储中,Entry 之间不会有缝隙)。
存储流程概述
本文的存储流程将采用经过简化后的 Bistcask 存储模型。Bitcask 是一种 LSM 类别下的简单的存储模型,其主要思想是通过顺序写和非原地更新(通过 append 新数据来更新或删除旧数据)节省磁盘随机寻道的开销,从而提高写吞吐量,但是这个特性也会造成读放大,适合写多读少的场景。
其基本的读写流程是:
- 从磁盘中读取数据文件在内存中生成索引(该步骤只在初始化时执行一次)
- 接收到读写命令之后通过在内存中的索引获取数据在磁盘中的位置
- 根据在第 2 步中拿到的具体位置信息在磁盘中进行数据读写,成功后更新内存中的索引
如下图所示:
由于数据是非原地更新,所以当接收到更新或删除命令时,只需要在文件末尾追加新的数据而不用管旧的数据,因为每次做数据读写都要先经过内存中的索引且索引指向的都是最新且有效的数据,所以该模型可以保证读写的正确性。
通过观察上图的存储模型,在实现层面上有两个问题需要解决:
- 存储的 key-value 数据的长度可以是任意的,这就导致每个 Entry 的长度不是固定的(如图中的浅蓝色 Entry 和 深蓝色 Entry)且相邻 Entry 之间没有缝隙(即 Entry 之间无分隔符,视图上看起来都是无差别的二进制数据),而我们通过索引只能知道数据在磁盘中的起始位置,如何在 Entry 长度不确定的情况下正确解码出数据(数据编码与解码问题)。
- 资源是有限的,不可能无止境地追加新数据而不管旧数据,需要定期地对无效的旧数据做清理,回收对应的资源(无效数据回收问题)。
下面将分别对这两个问题的解决方案进行介绍。
数据编码与解码
数据正确解码的前提是明确知道 Entry 的在磁盘中的起始位置和终止位置,然后根据设计好的编码模型进行反序列解码即可。Entry 的起始位置我们可以从索引中很容易地获取到,重点在于如何知道 Entry 的终止位置。最简单的实现方案是在索引中额外存储 Entry 序列化之后的长度字段,起始位置加上长度即可得到终止位置,但是相对于磁盘,内存的存储资源是更为宝贵的,