写这篇文章的目的,是为了帮助更多的人理解 rosedb,我会从零开始实现一个简单的包含 PUT、GET、DELETE 操作的 k-v 存储引擎。

你可以将其看做是一个简易版本的 rosedb,就叫它 minidb 吧(mini 版本的 rosedb)。

无论你是 Go 语言初学者,还是想进阶 Go 语言,或者是对 k-v 存储感兴趣,都可以尝试自己动手实现一下,我相信一定会对你帮助很大的。


说到存储,其实解决的一个核心问题就是,怎么存放数据,怎么取出数据。在计算机的世界里,这个问题会更加的多样化。

计算机当中有内存和磁盘,内存是易失性的,掉电之后存储的数据全部丢失,所以,如果想要系统崩溃再重启之后依然正常使用,就不得不将数据存储在非易失性介质当中,最常见的便是磁盘。

所以,针对一个单机版的 k-v,我们需要设计数据在内存中应该怎么存放,在磁盘中应该怎么存放。

当然,已经有很多优秀的前辈们去探究过了,并且已经有了经典的总结,主要将数据存储的模型分为了两类:B+ 树和 LSM 树。

本文的重点不是讲这两种模型,所以只做简单介绍。

B+ 树

B+ 树由二叉查找树演化而来,通过增加每层节点的数量,来降低树的高度,适配磁盘的页,尽量减少磁盘 IO 操作。

B+ 树查询性能比较稳定,在写入或更新时,会查找并定位到磁盘中的位置并进行原地操作,注意这里是随机 IO,并且大量的插入或删除还有可能触发页分裂和合并,写入性能一般,因此 B+ 树适合读多写少的场景。

LSM 树

LSM Tree(Log Structured Merge Tree,日志结构合并树)其实并不是一种具体的树类型的数据结构,而只是一种数据存储的模型,它的核心思想基于一个事实:顺序 IO 远快于随机 IO。

和 B+ 树不同,在 LSM 中,数据的插入、更新、删除都会被记录成一条日志,然后追加写入到磁盘文件当中,这样所有的操作都是顺序 IO,因此 LSM 比较适用于写多读少的场景。


看了前面的两种基础存储模型,相信你已经对如何存取数据有了基本的了解,而 minidb 基于一种更加简单的存储结构,总体上它和 LSM 比较类似。

我先不直接干巴巴的讲这个模型的概念,而是通过一个简单的例子来看一下 minidb 当中数据 PUT、GET、DELETE 的流程,借此让你理解这个简单的存储模型。

PUT

我们需要存储一条数据,分别是 key 和 value,首先,为预防数据丢失,我们会将这个 key 和 value 封装成一条记录(这里把这条记录叫做 Entry),追加到磁盘文件当中。Entry 的里面的内容,大致是 key、value、key 的大小、value 的大小、写入的时间。

所以磁盘文件的结构非常简单,就是多个 Entry 的集合。

磁盘更新完了,再更新内存,内存当中可以选择一个简单的数据结构,比如哈希表。哈希表的 key 对应存放的是 Entry 在磁盘中的位置,便于查找时进行获取。

这样,在 minidb 当中,一次数据存储的流程就完了,只有两个步骤:一次磁盘记录的追加,一次内存当中的索引更新。

GET

再来看 GET 获取数据,首先在内存当中的哈希表查找到 key 对应的索引信息,这其中包含了 value 存储在磁盘文件当中的位置,然后直接根据这个位置,到磁盘当中去取出 value 就可以了。

DEL

然后是删除操作,这里并不会定位到原记录进行删除,而还是将删除的操作封装成 Entry,追加到磁盘文件当中,只是这里需要标识一下 Entry 的类型是删除。

然后在内存当中的哈希表删除对应的 key 的索引信息,这样删除操作便完成了。

可以看到,不管是插入、查询、删除,都只有两个步骤:一次内存中的索引更新,一次磁盘文件的记录追加。所以无论数据规模如何, minidb 的写入性能十分稳定。

Merge

最后再来看一个比较重要的操作,前面说到,磁盘文件的记录是一直在追加写入的,这样会导致文件容量也一直在增加。并且对于同一个 key,可能会在文件中存在多条 Entry(回想一下,更新或删除 key 内容也会追加记录),那么在数据文件当中,其实存在冗余的 Entry 数据。

举一个简单的例子,比如针对 key A, 先后设置其 value 为 10、20、30,那么磁盘文件中就有三条记录:

此时 A 的最新值是 30,那么其实前两条记录已经是无效的了。

针对这种情况,我们需要定期合并数据文件,清理无效的 Entry 数据,这个过程一般叫做 merge。

merge 的思路也很简单,需要取出原数据文件的所有 Entry,将有效的 Entry 重新写入到一个新建的临时文件中,最后将原数据文件删除,临时文件就是新的数据文件了。

这就是 minidb 底层的数据存储模型,它的名字叫做 bitcask,当然 rosedb 采用的也是这种模型。它本质上属于类 LSM 的模型,核心思想是利用顺序 IO 来提升写性能,只不过在实现上,比 LSM 简单多了。


介绍完了底层的存储模型,就可以开始代码实现了,我将完整的代码实现放到了我的 Github 上面,地址:

https://github.com/roseduan/minidb

文章当中就截取部分关键的代码。

首先是打开数据库,需要先加载数据文件,然后取出文件中的 Entry 数据,还原索引状态,关键部分代码如下:

func Open(dirPath string) (*MiniDB, error) {// 如果数据库目录不存在,则新建一个if _, err := os.Stat(dirPath); os.IsNotExist(err) {if err := os.MkdirAll(dirPath, os.ModePerm); err != nil {return nil, err}}// 加载数据文件dbFile, err := NewDBFile(dirPath)if err != nil {return nil, err}db := &MiniDB{dbFile: dbFile,indexes: make(map[string]int64),dirPath: dirPath,}// 加载索引db.loadIndexesFromFile(dbFile)return db, nil
}

再来看看 PUT 方法,流程和上面的描述一样,先更新磁盘,写入一条记录,再更新内存:

func (db *MiniDB) Put(key []byte, value []byte) (err error) {offset := db.dbFile.Offset// 封装成 Entryentry := NewEntry(key, value, PUT)// 追加到数据文件当中err = db.dbFile.Write(entry)// 写到内存db.indexes[string(key)] = offsetreturn
}

GET 方法需要先从内存中取出索引信息,判断是否存在,不存在直接返回,存在的话从磁盘当中取出数据。

func (db *MiniDB) Get(key []byte) (val []byte, err error) {// 从内存当中取出索引信息offset, ok := db.indexes[string(key)]// key 不存在if !ok {return}// 从磁盘中读取数据var e *Entrye, err = db.dbFile.Read(offset)if err != nil && err != io.EOF {return}if e != nil {val = e.Value}return
}

DEL 方法和 PUT 方法类似,只是 Entry 被标识为了 DEL ,然后封装成 Entry 写到文件当中:

func (db *MiniDB) Del(key []byte) (err error) {// 从内存当中取出索引信息_, ok := db.indexes[string(key)]// key 不存在,忽略if !ok {return}// 封装成 Entry 并写入e := NewEntry(key, nil, DEL)err = db.dbFile.Write(e)if err != nil {return}// 删除内存中的 keydelete(db.indexes, string(key))return
}

最后是重要的合并数据文件操作,流程和上面的描述一样,关键代码如下:

func (db *MiniDB) Merge() error {// 读取原数据文件中的 Entryfor {e, err := db.dbFile.Read(offset)if err != nil {if err == io.EOF {break}return err}// 内存中的索引状态是最新的,直接对比过滤出有效的 Entryif off, ok := db.indexes[string(e.Key)]; ok && off == offset {validEntries = append(validEntries, e)}offset += e.GetSize()}if len(validEntries) > 0 {// 新建临时文件mergeDBFile, err := NewMergeDBFile(db.dirPath)if err != nil {return err}defer os.Remove(mergeDBFile.File.Name())// 重新写入有效的 entryfor _, entry := range validEntries {writeOff := mergeDBFile.Offseterr := mergeDBFile.Write(entry)if err != nil {return err}// 更新索引db.indexes[string(entry.Key)] = writeOff}// 删除旧的数据文件os.Remove(db.dbFile.File.Name())// 临时文件变更为新的数据文件os.Rename(mergeDBFile.File.Name(), db.dirPath+string(os.PathSeparator)+FileName)db.dbFile = mergeDBFile}return nil
}

除去测试文件,minidb 的核心代码只有 300 行,麻雀虽小,五脏俱全,它已经包含了 bitcask 这个存储模型的主要思想,并且也是 rosedb 的底层基础。

理解了 minidb 之后,基本上就能够完全掌握 bitcask 这种存储模型,多花点时间,相信对 rosedb 也能够游刃有余了。

进一步,如果你对 k-v 存储这方面感兴趣,可以更加深入的去研究更多相关的知识,bitcask 虽然简洁易懂,但是问题也不少,rosedb 在实践的过程当中,对其进行了一些优化,但目前还是有不少的问题存在。

有的人可能比较疑惑,bitcask 这种模型简单,是否只是一个玩具,在实际的生产环境中有应用吗?答案是肯定的。

bitcask 最初源于 Riak 这个项目的底层存储模型,而 Riak 是一个分布式 k-v 存储,在 NoSQL 的排名中也名列前茅:

豆瓣所使用的的分布式 k-v 存储,其实也是基于 bitcask 模型,并对其进行了很多优化。目前纯粹基于 bitcask 模型的 k-v 并不是很多,所以你可以多去看看 rosedb 的代码,可以提出自己的意见建议,一起完善这个项目。

最后,附上相关项目地址:

minidb:https://github.com/roseduan/minidb

rosedb:https://github.com/roseduan/rosedb

参考资料:

https://riak.com/assets/bitcask-intro.pdf

https://medium.com/@arpitbhayani/bitcask-a-log-structured-fast-kv-store-c6c728a9536b


题图:from wallheaven.cc

结语

最近一直在做数据库相关内容,期待一起交流学习~

从零实现一个 k-v 存储引擎相关推荐

  1. 基于持久内存的 单机上亿(128B)QPS -- 持久化 k/v 存储引擎

    文章目录 性能数据 设计背景 设计架构 Hash 索引结构 及 PMEM空间管理形态 基本API 及 实现 API 初始化流程 写流程 读流程 删除流程 PMEM Allocator设计 主要组件 空 ...

  2. 分布式K/V存储方案

    引用:http://www.oschina.net/p/cassandra Apache Cassandra是一套开源分布式Key-Value存储系统.它最初由Facebook开发,用于储存特别大的数 ...

  3. Apache Accumulo 1.9.3 发布,高性能 K/V 存储方案

    Apache Accumulo 1.9.3 发布了,Apache Accumulo 是一个可靠的.可伸缩的.高性能的排序分布式的 Key-Value 存储解决方案,基于单元访问控制以及可定制的服务器端 ...

  4. 存储引擎 K/V 分离下的index回写问题

    前言 近期在做on nvme hash引擎相关的事情,对于非全序的数据集的存储需求,相比于我们传统的LSM或者B-tree的数据结构来说 能够减少很多维护全序上的计算/存储资源.当然我们要保证hash ...

  5. KVell 单机k/v引擎:用最少的CPU 来调度Nvme的极致性能

    文章目录 前言 KVell背景 业界引擎使用Nvme的问题 CPU 会是 LSM-kv 存储的瓶颈 CPU 也会是 Btree-kv 存储的瓶颈 KVell 设计亮点 及 总体架构实现 KVell 设 ...

  6. badger 一个高性能的LSM K/V store

    大家好,给大家介绍一下, 新晋的高性能的 K/V数据库: badger. 这是 dgraph.io开发的一款基于 log structured merge (LSM) tree 的 key-value ...

  7. 一个项目中能提出哪些数据库优化_阿里资深技术专家曲山:优秀的数据库存储引擎应具备哪些能力?...

    云栖君导读:作为数据库的底盘,一个成熟的存储引擎如何实现高效数据存取?本文作者是阿里巴巴OLTP数据库团队资深技术专家--曲山.作为自研高性能.低成本存储引擎X-Engine的负责人,曲山眼中的优秀关 ...

  8. 从零打造一个Web地图引擎

    说到地图,大家一定很熟悉,平时应该都使用过百度地图.高德地图.腾讯地图等,如果涉及到地图相关的开发需求,也有很多选择,比如前面的几个地图都会提供一套js API,此外也有一些开源地图框架可以使用,比如 ...

  9. MyRocks: 为facebool 的社交图谱服务的LSM-tree存储引擎

    文章目录 概览 1. UDB 架构 2. UDB 表格式 3. Rocksdb:针对flash存储优化过的第三方库 3.1 Rocksdb架构 3.2 为什么选择Rocksdb 4. MyRocks ...

  10. 如何写MySQL存储引擎

    mysql教程:如何写MySQL存储引擎 [日期:2009-04-02]     来源:  作者: 在MySQL 5.1中开发一个存储引擎已经是比较方便了.所谓存储引擎实际上是按照MySQL的约定,提 ...

最新文章

  1. 人工智能项目:需要注意的七件事
  2. gperf的学习笔记(一)
  3. 并查集板子:acwing836. 合并集合
  4. 四层LVS与七层Nginx负载均衡的区别
  5. c语言题目集-田队写长单词
  6. POJ2373-Dividing the Path【单调队列优化dp】
  7. Multiavatar头像生成,要多少有多少
  8. BZOJ2017[USACO 2009 Nov Silver 1.A Coin Game]——DP+博弈论
  9. 矩阵sum_Matlab-sum与cumsum函数
  10. 催收评分卡(二)关于催收评分卡的血泪史
  11. DevExpress GridControl使用方法
  12. java里程碑之泛型--使用泛型
  13. ZeptoLab Code Rush 2015 B. Om Nom and Dark Park DFS
  14. 批量调取接口_调用API接口批量查手机归属地
  15. 电脑只能上微信QQ不能浏览网页(解决方法)
  16. 分享一个非常厉害的md5 解密网站
  17. maven pom profiles
  18. 租车小程序开发的必备功能
  19. 程序员PS基本(二):程序员切图最常使用的工具组-选择工具组
  20. 压缩包里的文件名可以这样隐藏起来

热门文章

  1. 如何成为一个高情商的人?
  2. 优思学院|什么是六西格玛?一文解答你对六西格玛最常见的疑问
  3. MFC+Opencv4+vs2017 显示图像 详细小白教程(不使用cvvImage)
  4. PictureSelector(强大的图片选择器)
  5. 机器学习之局部加权、岭回归和前向逐步回归
  6. 单片机:DAC数模转换实验(内含DAC介绍+PWM介绍+硬件设计+软件设计+原始代码)
  7. 猎豹wifi有linux,最全猎豹免费wifi手机连上了但上不了网的解决方法介绍
  8. js 根据id获取页面元素
  9. JQ-AJAX执行成功不执行success方法解决
  10. 算法设计三(4)——世界名画陈列馆问题