愿景
今年大部分业余时间都在nutsdb的开源贡献上,nutsdb是基于bitcask模型实现的持久化存储引擎,提供了诸如list,set等多种丰富的数据结构。近来很多小伙伴,其中也有一些我的好朋友陆陆续续加入到这个项目上来。为了帮助小伙伴们快速熟悉整个项目,我会把之前写的一些文章分享给他们,但是我觉得这样可能还是不太够,因为之前做的是关于性能优化的事情,文章是偏向于性能优化方面的思考。对于bitcask整体架构的解释,实现,感觉写着墨不多,意识到这一点之后,我就想写一篇文章来解释一下bitcask的核心概念,并且花了几个小时的时间写下了一个简单版本的demo(写unit test测试之后,几百行代码只有一个小bug,小窃喜)。在帮助大家了解了核心概念之后,就算是有了一个基本的入门了,后面可以更快的参与到项目中来。这是我写这篇文章的初衷,也同时欢迎更多的小伙伴参与到这个项目中来。
这篇文章会结合bitcask论文以及我的代码实现进行分析,主要是讲的是“怎么做”,对于“为什么”可能侧重并不多。另外该bitcask实现我已经开源到我的github上,可以在github上看到更多实现细节,链接:https://github.com/elliotchenzichang/tiny-bitcask ,欢迎star。
整体架构
该怎么开始这个故事呢,我思虑良久,决定从db的整体架构开始阐述,然后在自底向上的讲述每一个部分的实现细节。为什么是自底向上呢?因为从下往上讲可以从点到线,从线到面,慢慢的拨开云雾,看见一整个db的实现,会有一种世界在面前缓缓打开的感觉。另外,如果是从上到下来看结合代码讲的时候,可能有一些代码会变得不好解释。
言归正传,如上面所说我们要实现一个基于Bitcask模型的kv存储引擎,那么对于持久化存储引擎而言,数据的最终归宿是磁盘。而我们知道,程序是运行在内存中的,所以存储引擎提供需要做的就是以某种方式把用户给的数据存进磁盘,也以某种方式将用户的数据从磁盘里面拿出来使用。至于这些方式的设计的是否高效,就是存储引擎设计的艺术所在。其实粗略来看,存储引擎的整体架构整体如下图,内存中放置索引,可以直接(比如bitcask)或者间接(比如leveldb的SST的索引形式)的找到数据,磁盘中存储用户的数据,可以是同构的数据文件(比如bitcask全是data_file),也可以是异构的数据文件(比如leveldb的WAL log和SST)。

bitcask采用的是一种比较简单的形式,如下图所示,内存中会记录每条数据在磁盘中的位置,以及key和value的长度,这样就可以直接通过一次系统调用在数据存放的位置把它拿出来了。
大概讲述了整体的架构之后我们来看看代码实现中主要的对象有哪些,自底向上的看。
- Entry:代表db中一条数据的信息。
- Storage:与文件系统打交道的对象,包括了写入,读取数据。
- Index,索引,记录一条数据的具体信息,主要是数据在磁盘中的位置。
- db,db的实体。包含了db的各种操作,包括读取,写入数据。
接下来我们看看具体的实现,下面解析会结合代码分析,在代码的关键部分我已经写好了注释。系好安全带,发车了!
1. 数据的编码与解码
首先我们要讲的是,一条数据是以怎么样的形式存进磁盘的。磁盘才不理你是放进来的是什么东西,他只知道在他身上存放的是一堆二进制,至于那些二进制是什么,由放进来的应用程序来定义。大概逻辑如下图所示,应用程序需要自己实现对磁盘数据写入和读出时候的编码和解码。
在kv存储引擎中一条数据在磁盘中的是如下图所示。整体上来看我们会有一个meta,key,value,key和value就不必多说了,就是真实的数据部分。那么meta是什么呢?meta是这条数据的元数据,也就是起到描述作用的数据,比如key有多长,value有多长,在什么位置,以及写入数据的时间,其实这个时间戳可以理解为数据的版本,可以是物理时间,也可以是逻辑时间(逻辑时间需要自己实现)。meta的crc部分是做数据校验用的,因为磁盘有时候会出现一些意外,比如一个比特位上的数据发生了变化,从存储1变成0或者从0到1,在或者磁盘数据丢失,也是可能的,所以要加上crc在读出数据的时候再读计算出crc的值和存在磁盘里的crc做比较,如果不一致说明数据出现了问题。

下面是代码实现的节选,其中包含了Entry和Meta这两个主要数据结构的定义,以及数据编码的实现。在写入数据的时候,我们会将一个内存中的Entry对象Encode编码成字节数组然后将字节数据存进磁盘中,在读取数据的时候再将拿到的字节数组解码,这样就组成了我们的数据编解码过程。
// Entry代表数据。
type Entry struct {
key []byte
value []byte
meta *Meta
}
// Meta是元数据
type Meta struct {
crc uint32
position uint64
timeStamp uint64
keySize uint32
valueSize uint32
}
//这个方法的功能是将一个Entry对象编码成byte数组
func (e *Entry) Encode() []byte {
// size是meta+key+value的长度。
size := e.Size()
buf := make([]byte, size)
//以小端字节序将数字写入到字节数组中
binary.LittleEndian.PutUint64(buf[4:12], e.meta.position)
binary.Lit

本文详细介绍基于Bitcask模型的KV存储引擎的核心概念及其实现细节,包括数据编码、存储与索引、并发处理、数据合并及崩溃恢复等关键环节。
最低0.47元/天 解锁文章
3069

被折叠的 条评论
为什么被折叠?



