自底向上剖析 boltdb 源码(1)

2022-03-10 00:00:00 数据 节点 分支 叶子 柜子

作者:jaydenwen,腾讯 PCG 后台开发工程师

前言

本文采用自底向上的方式来介绍 boltdb 内部的实现原理。其实我们经常都在采用自底向上或者自顶向下这两种方式来思考和求解问题。

例如:我们阅读源码时,通常都是从顶层的接口点进去,然后层层深入内部。这其实本质上就是一种自顶向下的方式。再比如我们平常做开发时,都是先将系统进行拆分、解耦。然后一般都会采用从下而上或者从上而下的方式来进行开发迭代。 又比如在执行 OKR 的时候,我们通常都是先定目标、然后依据该目标再层层分解。后分解到可执行的单元为止。这其实是一种自顶向下的方式;在真正执行时,我们通常又是从原先分解到底层的原子单元开始执行,然后层层递进。终所有的原子单元执行完后,我们的目标也就实现了。这其实又是自底向上的完成任务方式。

前面所提到的上下其实是指某些事物、组件、目标内部是存在相互依赖、因果、先后、递进关系,而依赖方或者结果方则位于上,被依赖方、原因方位于下;个人认为自下而上的方式比较适合执行每一个任务或者解决子问题,每一层做完时,都可以独立的进行测试和验证。当我们层层自下而上开发。后将前面的所有东西拼接在一起时,就构成了一个完整的组件或者实现了该目标。

回到初的话题,为什么本文要采用自底向上的方式来写呢? 对于一个文件型数据库而言,所谓的上指的是暴露给用户侧的调用接口。所谓的下又指它的输出(数据)终要落到磁盘这种存储介质上。采用自底向上的方式的话,也就意味着我们先从磁盘这一层进行分析。然后逐步衍生到内存;再到用户接口这一层。层层之间是被依赖的一种关系。这样的话,其实就比较好理解了。在本文中,本人采用自底向上的方式来介绍。希望阅读完后,有一种自己从 0 到 1 构建了一个数据库的快感。

当然也可以采用自顶向下的方式来介绍,这时我们就需要在介绍上层时,先假设它所依赖的底层都已经就绪了,我们只分析当层内容。然后层层往下扩展。

之前和一位大佬进行过针对此问题的探讨,在不同的场景、不同的组件中。具体采用自底向上还是自顶向下来分析。见仁见智,也具体问题具体分析。

在开始主要内容之前,先交代一些题外话。

本文核心内容主要摘自个人上周完结的自底向上剖析 boltdb 源码 一书。属于原先书籍的精简版。精简版的目的在于保证核心内容完整的前提下,把不太重要的部分和代码进行裁剪。大家可以根据需要自行选择阅读在线版或者精简版。

关于为什么会产生该书的原因,感兴趣的童鞋可以点击下面文章进行查阅,此处不再过多赘述。

  1. gocn.vip/topics/11941
  2. studygolang.com/resourc

上述书籍完整内容目前有以下渠道可以阅读和交流。

  1. 书栈网在线 or app 端
    bookstack.cn/books/jayd
  2. 个人 github 博客
    jaydenwen123.github.io/

下面开始正文介绍。

1. boltdb 简要介绍

本章是我们的开篇,我们主要从以下几个方面做一个讲述。希望这章让大家认识一下 boltdb,知道它是什么?做什么?后续所有的内容都建立再此基础上,给大家详细介绍它内部是怎么做的。因此本章内容的定位是为后续章节做一个过渡和铺垫,对 boltdb 比较熟悉的童鞋可以直接跳过前三节,直接从第四节阅读。本章的主要内容从以下几个方面展开:

  1. boltdb 是什么?
  2. 为什么要分析 boltdb?
  3. boltdb 的简单用法
  4. boltdb 的整体数据组织结构

1.1 boltdb 是什么?

在用自己的话介绍 boltdb 之前,我们先看下 boltdb 官方是如何自我介绍的呢?

Bolt is a pure Go key/value store inspired by [Howard Chu's][hyc_symas][LMDB project][lmdb]. The goal of the project is to provide a simple,fast, and reliable database for projects that don't require a full database server such as Postgres or MySQL.

Since Bolt is meant to be used as such a low-level piece of functionality,simplicity is key. The API will be small and only focus on getting values and setting values. That's it.

看完了官方的介绍,接下来让我用一句话对 boltdb 进行介绍:

boltdb 是一个纯 go 编写的支持事务的文件型单机 kv 数据库。

下面对上述几个核心的关键词进行一一补充。

纯 go: 意味着该项目只由 golang 语言开发,不涉及其他语言的调用。因为大部分的数据库基本上都是由 c 或者 c++开发的,boltdb 是一款难得的 golang 编写的数据库。

支持事务: boltdb 数据库支持两类事务:读写事务只读事务。这一点就和其他 kv 数据库有很大区别。

文件型: boltdb 所有的数据都是存储在磁盘上的,所以它属于文件型数据库。这里补充一下个人的理解,在某种维度来看,boltdb 很像一个简陋版的 innodb 存储引擎。底层数据都存储在文件上,同时数据都涉及数据在内存和磁盘的转换。但不同的是,innodb 在事务上的支持比较强大。

单机: boltdb 不是分布式数据库,它是一款单机版的数据库。个人认为比较适合的场景是,用来做 wal 日志或者读多写少的存储场景。

kv 数据库: boltdb 不是 sql 类型的关系型数据库,它和其他的 kv 组件类似,对外暴露的是 kv 的接口,不过 boltdb 支持的数据类型 key 和 value 都是[]byte。

1.2 为什么要分析 boltdb?

前文介绍完了什么是 boltdb。那我们先扪心自问一下,为什么要学习、分析 boltdb 呢?闲的吗? 答案:当然不是。我们先看看其他几个人对这个问题是如何答复的。

github 用户 ZhengHe-MD 是这么答复的:

要达到好的学习效果,就要有输出。以我平时的工作节奏,在闲暇时间依葫芦画瓢写一个键值数据库不太现实。于是我选择将自己对源码阅读心得系统地记录下来,终整理成本系列文章,旨在尽我所能正确地描述 boltDB。 恰好我在多次尝试在网上寻找相关内容后,发现网上大多数的文章、视频仅仅是介绍 boltDB 的用法和特性。因此,也许本系列文章可以作为它们以及 boltDB 官方文档 的补充,帮助想了解它的人更快地、深入地了解 boltDB。 如果你和我一样是初学者,相信它对你会有所帮助;如果你是一名经验丰富的数据库工程师,也许本系列文章对你来说没有太多新意。

微信公众号作者 TheFutureIsOurs 是这么答复的: boltdb 源码阅读

近抽时间看了 boltdb 的源码,代码量不大(大概 4000 行左右),而且支持事务,结构也很清晰,由于比较稳定,已经归档,确实是学习数据库的佳选择。而且不少出名的开源项目在使用它,比如 etcd,InfluxDB 等。

下面我来以自身的角度来回答下这个问题:

首先在互联网里面,所有的系统、软件都离不开数据。而提到数据,我们就会想到数据的存储和数据检索。这些功能不就是一个数据库基本的吗。从而数据库在计算机的世界里面有着无比重要的位置。作为一个有梦想的程序员,总是想知其然并知其所以然。这个是驱动我决定看源码的原因之一。

其次近在组里高涨的系统学习 mysql、redis 的氛围下,我也加入了阵营。想着把这两块知识好好消化、整理一番。尤其是 mysql,大家主要还是以核心学习 innodb 存储引擎为目标。本人也不例外,在我看完了从根儿上理解 mysql后。整体上对 innodb 有了宏观和微观的了解和认识,但是更近一步去看 mysql 的代码。有几个难点:1.本人项目主要以 golang 为主。说实话看 c 和 c++的项目或多或少有些理解难度;2.mysql 作为上古神兽,虽然功能很完善,但是要想短期内看完源码基本上是不可能的,而工作之余的时间有限,因此性价比极低。而 boltdb 完美的符合了我的这两个要求。所以这就是选择 boltdb 的第二个原因,也是一个主要原因。

后还是想通过分析这个项目,在下面两个方面有所提升。

  1. 一方面让自己加深原先学习的理论知识;
  2. 另外一方面也能真正的了解工程上是如何运用的,理论结合实践,然后对存储引擎有一个清晰的认识;

介绍完了 boltdb 是什么?为什么要分析 boltdb 后,我们就正式进入主题了。让我们先以一个简单例子认识下 boltdb。

1.3 boltdb 的简单用法

其实 boltdb 的用法很简单,从其项目 github 的文档里面就可以看得出来。它本身的定位是 key/value(后面简称为 kv)存储的嵌入式数据库,因此那提到 kv 我们自然而然能想到的常用的操作,就是 set(k,v)和 get(k)了。确实如此 boltdb 也就是这么简单。

不过在详细介绍 boltdb 使用之前,我们先以日常生活中的一些场景来作为切入点,引入一些在 boltdb 中抽象出来的专属名词(DB、Bucket、Cursor、k/v 等),下面将进入正文,前面提到 boltdb 的使用确实很简单,就是 set 和 get。但它还在此基础上还做了一些额外封装。下面通过现实生活对比来介绍这些概念。

boltdb 本质就是存放数据的,那这和现实生活中的柜子就有点类似了,如果我们把 boltdb 看做是一个存放东西的柜子的话,它里面可以存放各种各样的东西,确实是的,但是我们想一想,所有东西都放在一起会不会有什么问题呢?

咦,如果我们把钢笔、铅笔、外套、毛衣、短袖、餐具这些都放在一个柜子里的话,会有啥问题呢?这对于特那些别喜欢收拾屋子,东西归类放置的人而言,简直就是一个不可容忍的事情,因为所有的东西都存放在一起,当东西多了以后就会显得杂乱无章。

在生活中我们都有分类、归类的习惯,例如对功能类似的东西(钢笔、铅笔、圆珠笔等)放一起,或者同类型的东西(短袖、长袖等)放一起。把前面的柜子通过隔板来隔开,分为几个小的小柜子,个柜子可以放置衣服,第二个柜子可以放置书籍和笔等。当然了,这是很久以前的做法了,现在买的柜子,厂家都已经将其内部通过不同的存放东西的规格做好了分隔。大家也就不用为这些琐事操心了。既然这样,那把分类、归类这个概念往计算机中迁移过来,尤其是对于存放数据的数据库 boltdb 中,它也需要有分类、归类的思想,因为归根到底,它也是由人创造出来的嘛。

好了到这儿,我们引入我们的三大名词了“DB”、“Bucket”、“k/v”

DB: 对应我们上面的柜子。

Bucket: 对应我们将柜子分隔后的小柜子或者抽屉了。

k/v: 对应我们放在抽屉里的每一件东西。为了方便我们后面使用的时候便捷,我们需要给每个东西都打上一个标记,这个标记是可以区分每件东西的,例如 k 可以是该物品的颜色、或者价格、或者购买日期等,v 就对应具体的东西啦。这样当我们后面想用的时候,就很容易找到。尤其是女同胞们的衣服和包包,哈哈

再此我们就可以得到一个大概的层次结构,一个柜子(DB)里面可以有多个小柜子(Bucket),每个小柜子里面存放的就是每个东西(k/v)啦。

那我们想一下,我们周末买了一件新衣服,回到家,我们要把衣服放在柜子里,那这时候需要怎么操作呢?

很简单啦,下面看看我们平常怎么做的。

步: 如果家里没有柜子,那就得先买一个柜子;

第二步: 在柜子里找找之前有没有放置衣服的小柜子,没有的话,那就分一块出来,总不能把新衣服和钢笔放在一块吧。

第三步: 有了放衣服的柜子,那就里面找找,如果之前都没衣服,直接把衣服打上标签,然后丢进去就 ok 啦;如果之前有衣服,那我们就需要考虑要怎么放了,随便放还是按照一定的规则来放。这里我猜大部分人还是会和我一样吧。喜欢按照一定的规则放,比如按照衣服的新旧来摆放,或者按照衣服的颜色来摆放,或者按照季节来摆放,或者按照价格来摆放。哈哈

我们在多想一下,周一早上起来我们要找一件衣服穿着去上班,那这时候我们又该怎么操作呢?

步: 去找家里存放东西的柜子,家里没柜子,那就连衣服都没了,尴尬...。所以我们肯定是有柜子的,对不对

第二步: 找到柜子了,然后再去找放置衣服的小柜子,因为衣服在小柜子存放着。

第三步: 找到衣服的柜子了,那就从里面找一件衣服了,找哪件呢!新买的?喜欢的?天气下雨了,穿厚一点的?天气升温了,穿薄一点的?今天没准可能要约会,穿有气质的?.....

那这时候根据不同场景来确定了规则,明确了我们要找的衣服的标签,找起来就会很快了。我们一下子就能定位到要穿的衣服了。嗯哼,这就是排序、索引的威力了

如果之前放置的衣服没有按照这些规则来摆放。那这时候就很悲剧了,就得挨个挨个找,然后自己选了。哈哈,有点全表扫描的味道了

啰里啰嗦扯了一大堆,就是为了给大家科普清楚,一些 boltdb 中比较重要的概念,让大家对比理解。降低理解难度。下面开始介绍 boltdb 是如何简单使用的。

使用无外乎两个操作:setget

func main() {
   // 我们的大柜子
   db, err := bolt.Open("./my.db", 0600, nil)
   if err != nil {
      panic(err)
   }
   defer db.Close()
   // 往小柜子里放东西
   err = db.Update(func(tx *bolt.Tx) error {
      //我们的小柜子
      bucket, err := tx.CreateBucketIfNotExists([]byte("user"))
      if err != nil {
         log.Fatalf("CreateBucketIfNotExists err:%s", err.Error())
         return err
      }
      //放入东西
      if err = bucket.Put([]byte("hello"), []byte("world")); err != nil {
         log.Fatalf("bucket Put err:%s", err.Error())
         return err
      }
      return nil
   })
   if err != nil {
      log.Fatalf("db.Update err:%s", err.Error())
   }
   // 从柜子里取东西
   err = db.View(func(tx *bolt.Tx) error {
      //找到柜子
      bucket := tx.Bucket([]byte("user"))
      //找东西
      val := bucket.Get([]byte("hello"))
      log.Printf("the get val:%s", val)
      val = bucket.Get([]byte("hello2"))
      log.Printf("the get val2:%s", val)
      return nil
   })
   if err != nil {
      log.Fatalf("db.View err:%s", err.Error())
   }
}

相关文章