ITPub博客

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

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

作者:飘絮絮絮丶 时间:2022-03-10 13:47:01 0 删除 编辑

作者: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 是如何简单使用的。

使用无外乎两个操作:set、get

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())
   }
}

1.4 boltdb 的整体数据组织结构

下面这幅图完整的展示了 boltdb 中数据在磁盘文件(file)、文件中的每页(page)上的存储格式以及内存(bucket、node)中 b+树形式的组织情况。先从整体上给大家展示一下,大家暂时看不懂不要紧,后面章节会详细的分析每一部分的内容。

1.5 总结

在此做一个小结,本章我们首先对 boltdb 进行一个简要的介绍,让大家知道 boltdb 是什么,做什么用的。接着又回答了为什么要分析 boltdb。在第三节中通过类比现实生活的场景给大家介绍了一下 boltdb 的简单用法。既然我们用起来了 boltdb,那我们就留下了一个悬念,它内部是如何运转的呢?在分析悬念之前,我们在第四节中,从整体对大家展示了 boltdb 中数据时如何组织存储的。后面的内容都是围绕着这张图来展开的。

2.boltdb 的核心数据结构分析

从一开始,boltdb 的定位就是一款文件数据库,顾名思义它的数据都是存储在磁盘文件上的,目前我们大部分场景使用的磁盘还是机械磁盘。而我们又知道数据落磁盘其实是一个比较慢的操作(此处的快慢是和操作内存想对比而言)。所以怎么样在这种硬件条件无法改变的情况下,如何提升性能就成了一个恒定不变的话题。而提升性能就不得不提到它的数据组织方式了。所以这部分我们主要来分析 boltdb 的核心数据结构。

我们都知道,操作磁盘之所以慢,是因为对磁盘的读写耗时主要包括:寻道时间+旋转时间+传输时间。而这儿的大头主要是在寻道时间上,因为寻道是需要移动磁头到对应的磁道上,通过马达驱动磁臂的移动是一种机械运动,比较耗时。我们往往对磁盘的操作都是随机读写,简而言之,随机读写的话,需要频繁移动磁头到对应的磁道。这种方式性能比较低。还有一种和它对应的方式:顺序读写。顺序读写的性能要比随机读写高很多。

因此,所谓的提升性能,无非就是尽可能的减少磁盘的随机读写,更大程度采用顺序读写的方式。这是主要矛盾,不管是 mysql 的 innodb 还是 boltdb 他们都是围绕这个核心来展开的。如何将用户写进来在内存中的数据尽可能采用顺序写的方式放进磁盘,同时在用户读时,将磁盘中保存的数据以尽可能少的 IO 调用次数加载到内存中,近而返回用户。这里面就涉及到具体的数据在磁盘、内存中的组织结构以及相互转换了。下面我们就对这一块进行详细的分析。

这里面主要包含几块内容:一个是它在磁盘上的数据组织结构 page、一个是它在内存中的数据组织结构 node、还有一个是 page 和 node 之间的相互转换关系。

这里先给大家直观的科普一点:
set 操作: 本质上对应的是 set->node->page->file 的过程
get 操作: 本质上对应的是 file->page->node->get 的过程

2.1 boltdb page 结构

在 boltdb 中,一个 db 对应一个真实的磁盘文件。而在具体的文件中,boltdb 又是按照以 page 为单位来读取和写入数据的,也就是说所有的数据在磁盘上都是按照页(page)来存储的,而此处的页大小是保持和操作系统对应的内存页大小一致,也就是 4k。每页由两部分数据组成:页头数据+真实数据,页头信息占 16 个字节,下面的页的结构定义

type pgid uint64
type page struct {
 // 页id 8字节
 id pgid
 // flags:页类型,可以是分支,叶子节点,元信息,空闲列表  2字节,该值的取值详细参见下面描述
 flags uint16
 // 个数 2字节,统计叶子节点、非叶子节点、空闲列表页的个数
 count uint16
 // 4字节,数据是否有溢出,主要在空闲列表上有用
 overflow uint32
 // 真实的数据
 ptr uintptr
}

其中,ptr 是一个无类型指针,它就是表示每页中真实的存储的数据地址。而其余的几个字段(id、flags、count、overflow)为我们前面提到的页头信息。 下图展现的是 boltdb 中 page 的数据存储方式。


在 boltdb 中,它把页划分为四类:

page 页类型 类型定义 类型值 用途

boltdb 通过定义的常量来描述

// 页头的大小:16字节
const pageHeaderSize = int(unsafe.Offsetof(((*page)(nil)).ptr))

const minKeysPerPage = 2

//分支节点页中每个元素所占的大小
const branchPageElementSize = int(unsafe.Sizeof(branchPageElement{}))
//叶子节点页中每个元素所占的大小
const leafPageElementSize = int(unsafe.Sizeof(leafPageElement{}))

const (
 branchPageFlag   = 0x01 //分支节点页类型
 leafPageFlag     = 0x02 //叶子节点页类型
 metaPageFlag     = 0x04 //元数据页类型
 freelistPageFlag = 0x10 //空闲列表页类型
)

同时每页都有一个方法来判断该页的类型,我们可以清楚的看到每页时通过其 flags 字段来标识页的类型。

// typ returns a human readable page type string used for debugging.
func (p *page) typ() string {
 if (p.flags & branchPageFlag) != 0 {
  return "branch"
 } else if (p.flags & leafPageFlag) != 0 {
  return "leaf"
 } else if (p.flags & metaPageFlag) != 0 {
  return "meta"
 } else if (p.flags & freelistPageFlag) != 0 {
  return "freelist"
 }
 return fmt.Sprintf("unknown<%02x>", p.flags)
}

下面我们一一对其数据结构进行分析。

2.2 元数据页

每页有一个 meta()方法,如果该页是元数据页的话,可以通过该方法来获取具体的元数据信息。

// meta returns a pointer to the metadata section of the page.
func (p *page) meta() *meta {
 // 将p.ptr转为meta信息
 return (*meta)(unsafe.Pointer(&p.ptr))
}

详细的元数据信息定义如下:

type meta struct {
 magic    uint32 //魔数
 version  uint32 //版本
 pageSize uint32 //page页的大小,该值和操作系统默认的页大小保持一致
 flags    uint32 //保留值,目前貌似还没用到
 root     bucket //所有小柜子bucket的根
 freelist pgid //空闲列表页的id
 pgid     pgid //元数据页的id
 txid     txid //最大的事务id
 checksum uint64 //用作校验的校验和
}

下图展现的是元信息存储方式。

下面我们重点关注该 meta 数据是如何写入到一页中的,以及如何从磁盘中读取 meta 信息并封装到 meta 中

1. meta->page

// write writes the meta onto a page.
func (m *meta) write(p *page) {
   if m.root.root >= m.pgid {
      panic(fmt.Sprintf("root bucket pgid (%d) above high water mark (%d)", m.root.root, m.pgid))
   } else if m.freelist >= m.pgid {
      panic(fmt.Sprintf("freelist pgid (%d) above high water mark (%d)", m.freelist, m.pgid))
   }

   // Page id is either going to be 0 or 1 which we can determine by the transaction ID.
   // 指定页id和页类型
   p.id = pgid(m.txid % 2)
   p.flags |= metaPageFlag

   // Calculate the checksum.
   m.checksum = m.sum64()
   // 这儿p.meta()返回的是p.ptr的地址,因此通过copy之后,meta信息就放到page中了
   m.copy(p.meta())
}

// copy copies one meta object to another.
func (m *meta) copy(dest *meta) {
   *dest = *m
}

2. page->meta

page.go
// meta returns a pointer to the metadata section of the page.
func (p *page) meta() *meta {
 // 将p.ptr转为meta信息
 return (*meta)(unsafe.Pointer(&p.ptr))
}

2.3 空闲列表页

空闲列表页中主要包含三个部分:所有已经可以重新利用的空闲页列表 ids、将来很快被释放掉的事务关联的页列表 pending、页 id 的缓存。详细定义在freelist.go文件中,下面给大家展示其空闲页的定义。

// freelist represents a list of all pages that are available for allocation.
// It also tracks pages that have been freed but are still in use by open transactions.
type freelist struct {
   // 已经可以被分配的空闲页
   ids []pgid // all free and available free page ids.
   // 将来很快能被释放的空闲页,部分事务可能在读或者写
   pending map[txid][]pgid // mapping of soon-to-be free page ids by tx.
   cache   map[pgid]bool   // fast lookup of all free and pending page ids.
}

// newFreelist returns an empty, initialized freelist.
func newFreelist() *freelist {
   return &freelist{
      pending: make(map[txid][]pgid),
      cache:   make(map[pgid]bool),
   }
}

下图展示的是空闲列表的存储方式。

1. freelist->page

将空闲列表转换成页信息,写到磁盘中,此处需要注意一个问题,页头中的 count 字段是一个 uint16,占两个字节,其最大可以表示 2^16 即 65536 个数字,当空闲页的个数超过 65535 时时,需要将 p.ptr 中的第一个字节用来存储其空闲页的个数,同时将 p.count 设置为 0xFFFF。否则不超过的情况下,直接用 count 来表示其空闲页的个数

// write writes the page ids onto a freelist page. All free and pending ids are
// saved to disk since in the event of a program crash, all pending ids will
// become free.
//将 freelist信息写入到p中
func (f *freelist) write(p *page) error {
 // Combine the old free pgids and pgids waiting on an open transaction.

 // Update the header flag.
 // 设置页头中的页类型标识
 p.flags |= freelistPageFlag

 // The page.count can only hold up to 64k elements so if we overflow that
 // number then we handle it by putting the size in the first element.

 lenids := f.count()
 if lenids == 0 {
  p.count = uint16(lenids)
 } else if lenids < 0xFFFF {
  p.count = uint16(lenids)
  // 拷贝到page的ptr中
  f.copyall(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[:])
 } else {
  // 有溢出的情况下,后面第一个元素放置其长度,然后再存放所有的pgid列表
  p.count = 0xFFFF
  ((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[0] = pgid(lenids)
  // 从第一个元素位置拷贝
  f.copyall(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[1:])
 }
 return nil
}

// copyall copies into dst a list of all free ids and all pending ids in one sorted list.
// f.count returns the minimum length required for dst.
func (f *freelist) copyall(dst []pgid) {
 // 首先把pending状态的页放到一个数组中,并使其有序
 m := make(pgids, 0, f.pending_count())
 for _, list := range f.pending {
  m = append(m, list...)
 }
 sort.Sort(m)
 // 合并两个有序的列表,最后结果输出到dst中
 mergepgids(dst, f.ids, m)
}

// mergepgids copies the sorted union of a and b into dst.
// If dst is too small, it panics.
// 将a和b按照有序合并成到dst中,a和b有序
func mergepgids(dst, a, b pgids) {
 ...
 // Assign lead to the slice with a lower starting value, follow to the higher value.
 lead, follow := a, b
 if b[0] < a[0] {
  lead, follow = b, a
 }

 // Continue while there are elements in the lead.
 for len(lead) > 0 {
  // Merge largest prefix of lead that is ahead of follow[0].
  n := sort.Search(len(lead), func(i int) bool { return lead[i] > follow[0] })
  merged = append(merged, lead[:n]...)
  if n >= len(lead) {
   break
  }

  // Swap lead and follow.
  lead, follow = follow, lead[n:]
 }
 // Append what's left in follow.
 _ = append(merged, follow...)
 ...
}

2. page->freelist

从磁盘中加载空闲页信息,并转为 freelist 结构,转换时,也需要注意其空闲页的个数的判断逻辑,当 p.count 为 0xFFFF 时,需要读取 p.ptr 中的第一个字节来计算其空闲页的个数。否则则直接读取 p.ptr 中存放的数据为空闲页 ids 列表

//从磁盘中的page初始化freelist
// read initializes the freelist from a freelist page.
func (f *freelist) read(p *page) {
 // If the page.count is at the max uint16 value (64k) then it's considered
 // an overflow and the size of the freelist is stored as the first element.
 idx, count := 0, int(p.count)
 if count == 0xFFFF {
  idx = 1
  // 用第一个uint64来存储整个count的值
  count = int(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[0])
 }

 // Copy the list of page ids from the freelist.
 if count == 0 {
  f.ids = nil
 } else {
  ids := ((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[idx:count]
  f.ids = make([]pgid, len(ids))
  copy(f.ids, ids)

  // Make sure they're sorted.
  sort.Sort(pgids(f.ids))
 }

 // Rebuild the page cache.
 f.reindex()
}

// reindex rebuilds the free cache based on available and pending free lists.
func (f *freelist) reindex() {
 f.cache = make(map[pgid]bool, len(f.ids))
 for _, id := range f.ids {
  f.cache[id] = true
 }
 for _, pendingIDs := range f.pending {
  for _, pendingID := range pendingIDs {
   f.cache[pendingID] = true
  }
 }
}

2.4 分支节点页

分支节点主要用来构建索引,方便提升查询效率。下面我们来看看 boltdb 的分支节点的数据是如何存储的。

1. 分支节点页中元素定义:

分支节点在存储时,一个分支节点页上会存储多个分支页元素即 branchPageElement。这个信息可以记做为分支页元素元信息。元信息中定义了具体该元素的页 id(pgid)、该元素所指向的页中存储的最小 key 的值大小、最小 key 的值存储的位置距离当前的元信息的偏移量 pos。下面是 branchPageElement 的详细定义:

// branchPageElement represents a node on a branch page.
type branchPageElement struct {
 pos   uint32 //该元信息和真实key之间的偏移量
 ksize uint32
 pgid  pgid
}

// key returns a byte slice of the node key.
func (n *branchPageElement) key() []byte {
 buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
 // pos~ksize
 return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos]))[:n.ksize]
}

2. 分支节点页 page 中获取下标为 index 的某一个 element 的信息和获取全部的 elements 信息

// branchPageElement retrieves the branch node by index
func (p *page) branchPageElement(index uint16) *branchPageElement {
 return &((*[0x7FFFFFF]branchPageElement)(unsafe.Pointer(&p.ptr)))[index]
}

// branchPageElements retrieves a list of branch nodes.
func (p *page) branchPageElements() []branchPageElement {
 if p.count == 0 {
  return nil
 }
 return ((*[0x7FFFFFF]branchPageElement)(unsafe.Pointer(&p.ptr)))[:]
}

下图展现的是非叶子节点存储方式。

在内存中,分支节点页和叶子节点页都是通过 node 来表示,只不过的区别是通过其 node 中的 isLeaf 这个字段来区分。下面和大家分析分支节点页 page 和内存中的 node 的转换关系。

下面在介绍具体的转换关系前,我们介绍一下内存中的分支节点和叶子节点是如何描述的。

// node represents an in-memory, deserialized page.
type node struct {
 bucket     *Bucket
 isLeaf     bool
 unbalanced bool
 spilled    bool
 key        []byte
 pgid       pgid
 parent     *node
 children   nodes
 inodes     inodes
}

在内存中,具体的一个分支节点或者叶子节点都被抽象为一个 node 对象,其中是分支节点还是叶子节点主要通通过其 isLeaf 字段来区分。下面对分支节点和叶子节点做两点说明:

  1. 对叶子节点而言,其没有 children 这个信息。同时也没有 key 信息。isLeaf 字段为 true,其上存储的 key、value 都保存在 inodes 中
  2. 对于分支节点而言,其具有 key 信息,同时 children 也不一定为空。isLeaf 字段为 false,同时该节点上的数据保存在 inode 中。

为了方便大家理解,node 和 page 的转换,下面大概介绍下 inode 和 nodes 结构。我们在下一章会详细介绍 node。

const (
 bucketLeafFlag = 0x01
)
type nodes []*node

func (s nodes) Len() int           { return len(s) }
func (s nodes) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s nodes) Less(i, j int) bool { return bytes.Compare(s[i].inodes[0].key, s[j].inodes[0].key) == -1 }

// inode represents an internal node inside of a node.
// It can be used to point to elements in a page or point
// to an element which hasn't been added to a page yet.
type inode struct {
 // 表示是否是子桶叶子节点还是普通叶子节点。如果flags值为1表示子桶叶子节点,否则为普通叶子节点
 flags uint32
 // 当inode为分支元素时,pgid才有值,为叶子元素时,则没值
 pgid  pgid
 key   []byte
 // 当inode为分支元素时,value为空,为叶子元素时,才有值
 value []byte
}

type inodes []inode

3. page->node

通过分支节点页来构建 node 节点

// 根据page来初始化node
// read initializes the node from a page.
func (n *node) read(p *page) {
 n.pgid = p.id
 n.isLeaf = ((p.flags & leafPageFlag) != 0)
 n.inodes = make(inodes, int(p.count))

 for i := 0; i < int(p.count); i++ {
  inode := &n.inodes[i]
  if n.isLeaf {
   // 获取第i个叶子节点
   elem := p.leafPageElement(uint16(i))
   inode.flags = elem.flags
   inode.key = elem.key()
   inode.value = elem.value()
  } else {
   // 树枝节点
   elem := p.branchPageElement(uint16(i))
   inode.pgid = elem.pgid
   inode.key = elem.key()
  }
  _assert(len(inode.key) > 0, "read: zero-length inode key")
 }

 // Save first key so we can find the node in the parent when we spill.
 if len(n.inodes) > 0 {
  // 保存第1个元素的值
  n.key = n.inodes[0].key
  _assert(len(n.key) > 0, "read: zero-length node key")
 } else {
  n.key = nil
 }
}

4. node->page

将 node 中的数据写入到 page 中

// write writes the items onto one or more pages.
// 将node转为page
func (n *node) write(p *page) {
 // Initialize page.
 // 判断是否是叶子节点还是非叶子节点
 if n.isLeaf {
  p.flags |= leafPageFlag
 } else {
  p.flags |= branchPageFlag
 }

 // 这儿叶子节点不可能溢出,因为溢出时,会分裂
 if len(n.inodes) >= 0xFFFF {
  panic(fmt.Sprintf("inode overflow: %d (pgid=%d)", len(n.inodes), p.id))
 }
 p.count = uint16(len(n.inodes))

 // Stop here if there are no items to write.
 if p.count == 0 {
  return
 }

 // Loop over each item and write it to the page.
 // b指向的指针为提逃过所有item头部的位置
 b := (*[maxAllocSize]byte)(unsafe.Pointer(&p.ptr))[n.pageElementSize()*len(n.inodes):]
 for i, item := range n.inodes {
  _assert(len(item.key) > 0, "write: zero-length inode key")

  // Write the page element.
  // 写入叶子节点数据
  if n.isLeaf {
   elem := p.leafPageElement(uint16(i))
   elem.pos = uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem)))
   elem.flags = item.flags
   elem.ksize = uint32(len(item.key))
   elem.vsize = uint32(len(item.value))
  } else {
   // 写入分支节点数据
   elem := p.branchPageElement(uint16(i))
   elem.pos = uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem)))
   elem.ksize = uint32(len(item.key))
   elem.pgid = item.pgid
   _assert(elem.pgid != p.id, "write: circular dependency occurred")
  }

  // If the length of key+value is larger than the max allocation size
  // then we need to reallocate the byte array pointer.
  //
  // See: https://github.com/boltdb/bolt/pull/335
  klen, vlen := len(item.key), len(item.value)
  if len(b) < klen+vlen {
   b = (*[maxAllocSize]byte)(unsafe.Pointer(&b[0]))[:]
  }

  // Write data for the element to the end of the page.
  copy(b[0:], item.key)
  b = b[klen:]
  copy(b[0:], item.value)
  b = b[vlen:]
 }

 // DEBUG ONLY: n.dump()
}

2.5 叶子节点页

叶子节点主要用来存储实际的数据,也就是 key+value 了。下面看看具体的 key+value 是如何设计的。 在 boltdb 中,每一对 key/value 在存储时,都有一份元素元信息,也就是 leafPageElement。其中定义了 key 的长度、value 的长度、具体存储的值距离元信息的偏移位置 pos。

// leafPageElement represents a node on a leaf page.
// 叶子节点既存储key,也存储value
type leafPageElement struct {
 flags uint32 //该值主要用来区分,是子桶叶子节点元素还是普通的key/value叶子节点元素。flags值为1时表示子桶。否则为key/value
 pos   uint32
 ksize uint32
 vsize uint32
}

// 叶子节点的key
// key returns a byte slice of the node key.
func (n *leafPageElement) key() []byte {
 buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
 // pos~ksize
 return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos]))[:n.ksize:n.ksize]
}

// 叶子节点的value
// value returns a byte slice of the node value.
func (n *leafPageElement) value() []byte {
 buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
 // key:pos~ksize
 // value:pos+ksize~pos+ksize+vsize
 return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos+n.ksize]))[:n.vsize:n.vsize]
}

下面是具体在叶子节点的 page 中获取下标为 index 的某个 key/value 的元信息。根据其元信息,就可以进一步获取其存储的 key 和 value 的值了,具体方法可以看上面的 key()和 value()

// leafPageElement retrieves the leaf node by index
func (p *page) leafPageElement(index uint16) *leafPageElement {
 n := &((*[0x7FFFFFF]leafPageElement)(unsafe.Pointer(&p.ptr)))[index]

 // 最原始的指针:unsafe.Pointer(&p.ptr)
 // 将其转为(*[0x7FFFFFF]leafPageElement)(unsafe.Pointer(&p.ptr))
 // 然后再取第index个元素 ((*[0x7FFFFFF]leafPageElement)(unsafe.Pointer(&p.ptr)))[index]
 // 最后返回该元素指针 &((*[0x7FFFFFF]leafPageElement)(unsafe.Pointer(&p.ptr)))[index]

 // ((*[0x7FFFFFF]leafPageElement)(unsafe.Pointer(&p.ptr)))
 // (*[0x7FFFFFF]leafPageElement)(unsafe.Pointer(&p.ptr))==>[]leafPageElement
 // &leafElements[index]
 return n
}

// leafPageElements retrieves a list of leaf nodes.
func (p *page) leafPageElements() []leafPageElement {
 if p.count == 0 {
  return nil
 }
 return ((*[0x7FFFFFF]leafPageElement)(unsafe.Pointer(&p.ptr)))[:]
}

下图展现的是叶子节点存储方式。

其具体叶子节点页 page 转换成 node 时的转变过程如同分支节点转换的方法一样,此处就不做赘述,可以参考 2.4 节介绍的 read()和 write()方法

2.6 总结

本章中我们重点分析了 boltdb 中的核心数据结构(page、freelist、meta、node)以及他们之间的相互转化。 在底层磁盘上存储时,boltdb 是按照页的单位来存储实际数据的,页的大小取自于它运行的操作系统的页大小。在 boltdb 中,根据存储的数据的类型不同,将页 page 整体上分为 4 大类:
1. 元信息页(meta page)
2. 空闲列表页(freelist page)
3. 分支节点页(branch page)
4. 叶子节点页(leaf page)

在 page 的头信息中通过 flags 字段来区分。

在内存中同样有对应的结构来映射磁盘上的上述几种页。如元信息 meta、空闲列表 freelist、分支/叶子节点 node(通过 isLeaf 区分分支节点还是叶子节点)。我们在每一节中先详细介绍其数据结构的定义。接着再重点分析在内存和磁盘上该类型的页时如何进行转换的。可以准确的说,数据结构属于 boltdb 核心中的核心。梳理清楚了每个数据结构存储的具体数据和格式后。下一章我们将重点分析其另外两个核心结构 bucket 和 node。

3. boltdb 的 b+树(Bucket、node)分析

在第一章我们提到在 boltdb 中,一个 db 对应底层的一个磁盘文件。一个 db 就像一个大柜子一样,其中可以被分隔多个小柜子,用来存储同类型的东西。每个小柜子在 boltdb 中就是 Bucket 了。bucket 英文为桶。很显然按照字面意思来理解,它在生活中也是存放数据的一种容器。目前为了方便大家理解,在 boltdb 中的 Bucket 可以粗略的认为,它里面主要存放的内容就是我们的 k/v 键值对啦。但这儿其实不准确,后面会详细说明。下面详细进行分析 Bucket。在 boltdb 中定义有 bucket、Bucket 两个结构。我们在此处所指的 Bucket 都是指 Bucket 哈。请大家注意!

3.1 boltdb 的 Bucket 结构

先来看官方文档的一段描述 Bucket 的话。

Bucket represents a collection of key/value pairs inside the database.

下面是 Bucket 的详细定义,本节我们先暂时忽略事务 Tx,后面章节会详细介绍事务

// 16 byte
const bucketHeaderSize = int(unsafe.Sizeof(bucket{}))

// Bucket represents a collection of key/value pairs inside the database.
// 一组key/value的集合,也就是一个b+树
type Bucket struct {
 *bucket //在内联时bucket主要用来存储其桶的value并在后面拼接所有的元素,即所谓的内联
 tx       *Tx                // the associated transaction
 buckets  map[string]*Bucket // subbucket cache
 page     *page              // inline page reference,内联页引用
 rootNode *node              // materialized node for the root page.
 nodes    map[pgid]*node     // node cache

 // Sets the threshold for filling nodes when they split. By default,
 // the bucket will fill to 50% but it can be useful to increase this
 // amount if you know that your write workloads are mostly append-only.
 //
 // This is non-persisted across transactions so it must be set in every Tx.
 // 填充率,默认值0.5
 FillPercent float64
}

// bucket represents the on-file representation of a bucket.
// This is stored as the "value" of a bucket key. If the bucket is small enough,
// then its root page can be stored inline in the "value", after the bucket
// header. In the case of inline buckets, the "root" will be 0.
type bucket struct {
 root     pgid   // page id of the bucket's root-level page
 sequence uint64 // monotonically incrementing, used by NextSequence()
}

// newBucket returns a new bucket associated with a transaction.
func newBucket(tx *Tx) Bucket {
 var b = Bucket{tx: tx, FillPercent: DefaultFillPercent}
 if tx.writable {
  b.buckets = make(map[string]*Bucket)
  b.nodes = make(map[pgid]*node)
 }
 return b
}

下图展现的是数据在 bucket 中的存储方式。

上面是一个 Bucket 的定义,在开始下面的内容前,我们先提前介绍一下另一个角色 Cursor,因为后面会频繁的用到它。大家在这里先知道,一个 Bucket 就是一个 b+树就可以了。我们后面会对其进行详细的分析。

3.2 Bucket 遍历之 Cursor

本节我们先做一节内容的铺垫,暂时不讲如何创建、获取、删除一个 Bucket。而是介绍一个 boltdb 中的新对象 Cursor。

答案是:所有的上述操作都是建立在首先定位到一个 Bucket 所属的位置,然后才能对其进行操作。而定位一个 Bucket 的功能就是由 Cursor 来完成的。所以我们先这一节给大家介绍一下 boltdb 中的 Cursor。

我们先看下官方文档对 Cursor 的描述

Cursor represents an iterator that can traverse over all key/value pairs in a bucket in sorted order.

用大白话讲,既然一个 Bucket 逻辑上是一颗 b+树,那就意味着我们可以对其进行遍历。前面提到的 set、get 操作,无非是要在 Bucket 上先找到合适的位置,然后再进行操作。而“找”这个操作就是交由 Cursor 来完成的。简而言之对 Bucket 这颗 b+树的遍历工作由 Cursor 来执行。一个 Bucket 对象关联一个 Cursor。下面我们先看看 Bucket 和 Cursor 之间的关系。

// Cursor creates a cursor associated with the bucket.
// The cursor is only valid as long as the transaction is open.
// Do not use a cursor after the transaction is closed.
func (b *Bucket) Cursor() *Cursor {
 // Update transaction statistics.
 b.tx.stats.CursorCount++

 // Allocate and return a cursor.
 return &Cursor{
  bucket: b,
  stack:  make([]elemRef, 0),
 }
}

3.2.1 Cursor 结构

从上面可以清楚的看到,在获取一个游标 Cursor 对象时,会将当前的 Bucket 对象传进去,同时还初始化了一个栈对象,结合数据结构中学习的树的知识。我们也就知道,它的内部就是对树进行遍历。下面我们详细介绍 Cursor 这个人物。

// Cursor represents an iterator that can traverse over all key/value pairs in a bucket in sorted order.
// Cursors see nested buckets with value == nil.
// Cursors can be obtained from a transaction and are valid as long as the transaction is open.
//
// Keys and values returned from the cursor are only valid for the life of the transaction.
//
// Changing data while traversing with a cursor may cause it to be invalidated
// and return unexpected keys and/or values. You must reposition your cursor
// after mutating data.
type Cursor struct {
 bucket *Bucket
 // 保存遍历搜索的路径
 stack []elemRef
}

// elemRef represents a reference to an element on a given page/node.
type elemRef struct {
 page  *page
 node  *node
 index int
}

3.2.2 Cursor 对外接口

下面我们看一下 Cursor 对外暴露的接口有哪些。看之前也可以心里先想一下。针对一棵树我们需要哪些遍历接口呢?

主体也就是三类:定位到某一个元素的位置、在当前位置从前往后找、在当前位置从后往前找。

// First moves the cursor to the first item in the bucket and returns its key and value.
// If the bucket is empty then a nil key and value are returned.
// The returned key and value are only valid for the life of the transaction.
func (c *Cursor) First() (key []byte, value []byte)

// Last moves the cursor to the last item in the bucket and returns its key and value.
//...
func (c *Cursor) Last() (key []byte, value []byte)

// Next moves the cursor to the next item in the bucket and returns its key and value.
//...
func (c *Cursor) Next() (key []byte, value []byte)

// Prev moves the cursor to the previous item in the bucket and returns its key and value.
//...
func (c *Cursor) Prev() (key []byte, value []byte)

// Delete removes the current key/value under the cursor from the bucket.
// Delete fails if current key/value is a bucket or if the transaction is not writable.
func (c *Cursor) Delete() error

// Seek moves the cursor to a given key and returns it.
// If the key does not exist then the next key is used. If no keys follow, a nil key is returned.
// The returned key and value are only valid for the life of the transaction.
func (c *Cursor) Seek(seek []byte) (key []byte, value []byte)

3.2.3 Seek(key)、First()、Last()实现分析

由于篇幅有限,关于 Seek()、First()、Last()、Next()、Prev()、Delete()这三个方法的内部实现。代码就不贴出来了。大家感兴趣可以点击此处或者点击此处 进入对应章节阅读。

3.3 node 节点的相关操作

在开始分析 node 节点之前,我们先看一下官方对 node 节点的描述

node represents an in-memory, deserialized page

一个 node 节点,既可能是叶子节点,也可能是根节点,也可能是分支节点。是物理磁盘上读取进来的页 page 的内存表现形式。

3.3.1 node 节点的定义

// node represents an in-memory, deserialized page.
type node struct {
 bucket     *Bucket // 关联一个桶
 isLeaf     bool
 unbalanced bool   // 值为true的话,需要考虑页合并
 spilled    bool   // 值为true的话,需要考虑页分裂
 key        []byte // 对于分支节点的话,保留的是最小的key
 pgid       pgid   // 分支节点关联的页id
 parent     *node  // 该节点的parent
 children   nodes  // 该节点的孩子节点
 inodes     inodes // 该节点上保存的索引数据
}

// inode represents an internal node inside of a node.
// It can be used to point to elements in a page or point
// to an element which hasn't been added to a page yet.
type inode struct {
 // 表示是否是子桶叶子节点还是普通叶子节点。如果flags值为1表示子桶叶子节点,否则为普通叶子节点
 flags uint32
 // 当inode为分支元素时,pgid才有值,为叶子元素时,则没值
 pgid pgid
 key  []byte
 // 当inode为分支元素时,value为空,为叶子元素时,才有值
 value []byte
}

type inodes []inode

3.3.2 node 节点和 page 转换

在 node 对象上有两个方法,read(page)、write(page),其中read(page)方法是用来通过 page 构建一个 node 节点;而write(page)方法则是将当前的 node 节点写入到 page 中,我们在前面他提到了 node 节点和 page 节点的相互转换,大家可以回到 2.4 节内容进行回顾。此处不再重复。

3.3.3 node 节点的增删改查

put(k,v)

// put inserts a key/value.
// 如果put的是一个key、value的话,不需要指定pgid。
// 如果put的一个树枝节点,则需要指定pgid,不需要指定value
func (n *node) put(oldKey, newKey, value []byte, pgid pgid, flags uint32) {
 if pgid >= n.bucket.tx.meta.pgid {
  panic(fmt.Sprintf("pgid (%d) above high water mark (%d)", pgid, n.bucket.tx.meta.pgid))
 } else if len(oldKey) <= 0 {
  panic("put: zero-length old key")
 } else if len(newKey) <= 0 {
  panic("put: zero-length new key")
 }

 // Find insertion index.
 index := sort.Search(len(n.inodes), func(i int) bool { return bytes.Compare(n.inodes[i].key, oldKey) != -1 })

 // Add capacity and shift nodes if we don't have an exact match and need to insert.
 exact := (len(n.inodes) > 0 && index < len(n.inodes) && bytes.Equal(n.inodes[index].key, oldKey))
 if !exact {
  n.inodes = append(n.inodes, inode{})
  copy(n.inodes[index+1:], n.inodes[index:])
 }

 inode := &n.inodes[index]
 inode.flags = flags
 inode.key = newKey
 inode.value = value
 inode.pgid = pgid
 _assert(len(inode.key) > 0, "put: zero-length inode key")
}

get(k)

在 node 中,没有 get(k)的方法,其本质是在 Cursor 中就返回了 get 的数据。大家可以看看 Cursor 中的 keyValue()方法。

del(k)

// del removes a key from the node.
func (n *node) del(key []byte) {
 // Find index of key.
 index := sort.Search(len(n.inodes), func(i int) bool { return bytes.Compare(n.inodes[i].key, key) != -1 })

 // Exit if the key isn't found.
 if index >= len(n.inodes) || !bytes.Equal(n.inodes[index].key, key) {
  return
 }

 // Delete inode from the node.
 n.inodes = append(n.inodes[:index], n.inodes[index+1:]...)

 // Mark the node as needing rebalancing.
 n.unbalanced = true
}

3.3.4 node 节点的分裂和合并

上面我们看了对 node 节点的操作,包括 put 和 del 方法。经过这些操作后,可能会导致当前的 page 填充度过高或者过低。因此就引出了 node 节点的分裂和合并。下面简单介绍下什么是分裂和合并。

分裂: 当一个 node 中的数据过多时,最简单就是当超过了 page 的填充度时,就需要将当前的 node 拆分成两个,也就是底层会将一页数据拆分存放到两页中。具体实现在spill()方法中。

spill writes the nodes to dirty pages and splits nodes as it goes. Returns an error if dirty pages cannot be allocated.

合并: 当删除了一个或者一批对象时,此时可能会导致一页数据的填充度过低,此时空间可能会浪费比较多。所以就需要考虑对页之间进行数据合并。具体实现在rebalance()方法中。

rebalance attempts to combine the node with sibling nodes if the node fill size is below a threshold or if there are not enough keys.

由于内容过长,此处代码就不贴出来了。关于该部分的代码分析大家感兴趣可以点击此处 或者点击此处 进行阅读。

3.4 Bucket 的相关操作

前面我们分析完了如何遍历、查找一个 Bucket 之后,下面我们来看看如何创建、获取、删除一个 Bucket 对象。

3.4.1 创建一个 Bucket

1. CreateBucketIfNotExists()、CreateBucket()分析

根据指定的 key 来创建一个 Bucket,如果指定 key 的 Bucket 已经存在,则会报错。如果指定的 key 之前有插入过元素,也会报错。否则的话,会在当前的 Bucket 中找到合适的位置,然后新建一个 Bucket 插入进去,最后返回给客户端。

// CreateBucketIfNotExists creates a new bucket if it doesn't already exist and returns a reference to it.
// Returns an error if the bucket name is blank, or if the bucket name is too long.
// The bucket instance is only valid for the lifetime of the transaction.
func (b *Bucket) CreateBucketIfNotExists(key []byte) (*Bucket, error) {
 child, err := b.CreateBucket(key)
 if err == ErrBucketExists {
  return b.Bucket(key), nil
 } else if err != nil {
  return nil, err
 }
 return child, nil
}

// CreateBucket creates a new bucket at the given key and returns the new bucket.
// Returns an error if the key already exists, if the bucket name is blank, or if the bucket name is too long.
// The bucket instance is only valid for the lifetime of the transaction.
func (b *Bucket) CreateBucket(key []byte) (*Bucket, error) {
 // ...
 // 省去异常检查逻辑

 // Move cursor to correct position.
 // 拿到游标
 c := b.Cursor()
 // 开始遍历、找到合适的位置
 k, _, flags := c.seek(key)

 // Return an error if there is an existing key.
 if bytes.Equal(key, k) {
  // 是桶,已经存在了
  if (flags & bucketLeafFlag) != 0 {
   return nil, ErrBucketExists
  }
  // 不是桶、但key已经存在了
  return nil, ErrIncompatibleValue
 }

 // Create empty, inline bucket.
 var bucket = Bucket{
  bucket:      &bucket{},
  rootNode:    &node{isLeaf: true},
  FillPercent: DefaultFillPercent,
 }
 // 拿到bucket对应的value
 var value = bucket.write()

 // Insert into node.
 key = cloneBytes(key)
 // 插入到inode中
 // c.node()方法会在内存中建立这棵树,调用n.read(page)
 c.node().put(key, key, value, 0, bucketLeafFlag)

 // Since subbuckets are not allowed on inline buckets, we need to
 // dereference the inline page, if it exists. This will cause the bucket
 // to be treated as a regular, non-inline bucket for the rest of the tx.
 b.page = nil

 //根据key获取一个桶
 return b.Bucket(key), nil
}

// write allocates and writes a bucket to a byte slice.
// 内联桶的话,其value中bucketHeaderSize后面的内容为其page的数据
func (b *Bucket) write() []byte {
 // Allocate the appropriate size.
 var n = b.rootNode
 var value = make([]byte, bucketHeaderSize+n.size())

 // Write a bucket header.
 var bucket = (*bucket)(unsafe.Pointer(&value[0]))
 *bucket = *b.bucket

 // Convert byte slice to a fake page and write the root node.
 var p = (*page)(unsafe.Pointer(&value[bucketHeaderSize]))
 // 将该桶中的元素压缩存储,放在value中
 n.write(p)

 return value
}


// node returns the node that the cursor is currently positioned on.
func (c *Cursor) node() *node {
 _assert(len(c.stack) > 0, "accessing a node with a zero-length cursor stack")

 // If the top of the stack is a leaf node then just return it.
 if ref := &c.stack[len(c.stack)-1]; ref.node != nil && ref.isLeaf() {
  return ref.node
 }

 // Start from root and traverse down the hierarchy.
 var n = c.stack[0].node
 if n == nil {
  n = c.bucket.node(c.stack[0].page.id, nil)
 }
 // 非叶子节点
 for _, ref := range c.stack[:len(c.stack)-1] {
  _assert(!n.isLeaf, "expected branch node")
  n = n.childAt(int(ref.index))
 }
 _assert(n.isLeaf, "expected leaf node")
 return n
}

// put inserts a key/value.
// 如果put的是一个key、value的话,不需要指定pgid。
// 如果put的一个树枝节点,则需要指定pgid,不需要指定value
func (n *node) put(oldKey, newKey, value []byte, pgid pgid, flags uint32) {
 put方法的具体实现3.3节中的put(k,v)。
}

3.4.2 获取一个 Bucket

根据指定的 key 来获取一个 Bucket。如果找不到则返回 nil。

// Bucket retrieves a nested bucket by name.
// Returns nil if the bucket does not exist.
// The bucket instance is only valid for the lifetime of the transaction.
func (b *Bucket) Bucket(name []byte) *Bucket {
 if b.buckets != nil {
  if child := b.buckets[string(name)]; child != nil {
   return child
  }
 }
 // Move cursor to key.
 // 根据游标找key
 c := b.Cursor()
 k, v, flags := c.seek(name)
 // Return nil if the key doesn't exist or it is not a bucket.
 if !bytes.Equal(name, k) || (flags&bucketLeafFlag) == 0 {
  return nil
 }
 // Otherwise create a bucket and cache it.
 // 根据找到的value来打开桶。
 var child = b.openBucket(v)
 // 加速缓存的作用
 if b.buckets != nil {
  b.buckets[string(name)] = child
 }

 return child
}

// Helper method that re-interprets a sub-bucket value
// from a parent into a Bucket
func (b *Bucket) openBucket(value []byte) *Bucket {
 var child = newBucket(b.tx)

 // If unaligned load/stores are broken on this arch and value is
 // unaligned simply clone to an aligned byte array.
 unaligned := brokenUnaligned && uintptr(unsafe.Pointer(&value[0]))&3 != 0

 if unaligned {
  value = cloneBytes(value)
 }

 // If this is a writable transaction then we need to copy the bucket entry.
 // Read-only transactions can point directly at the mmap entry.
 if b.tx.writable && !unaligned {
  child.bucket = &bucket{}
  *child.bucket = *(*bucket)(unsafe.Pointer(&value[0]))
 } else {
  child.bucket = (*bucket)(unsafe.Pointer(&value[0]))
 }

 // Save a reference to the inline page if the bucket is inline.
 // 内联桶
 if child.root == 0 {
  child.page = (*page)(unsafe.Pointer(&value[bucketHeaderSize]))
 }

 return &child
}

3.4.3 删除一个 Bucket

DeleteBucket()方法用来删除一个指定 key 的 Bucket。其内部实现逻辑是先递归的删除其子桶。然后再释放该 Bucket 的 page,并最终从叶子节点中移除

// DeleteBucket deletes a bucket at the given key.
// Returns an error if the bucket does not exists, or if the key represents a non-bucket value.
func (b *Bucket) DeleteBucket(key []byte) error {
 //异常逻辑检查
 // Move cursor to correct position.
 c := b.Cursor()
 k, _, flags := c.seek(key)

 // Return an error if bucket doesn't exist or is not a bucket.
 if !bytes.Equal(key, k) {
  return ErrBucketNotFound
 } else if (flags & bucketLeafFlag) == 0 {
  return ErrIncompatibleValue
 }

 // Recursively delete all child buckets.
 child := b.Bucket(key)
 // 将该桶下面的所有桶都删除
 err := child.ForEach(func(k, v []byte) error {
  if v == nil {
   if err := child.DeleteBucket(k); err != nil {
    return fmt.Errorf("delete bucket: %s", err)
   }
  }
  return nil
 })
 if err != nil {
  return err
 }

 // Remove cached copy.
 delete(b.buckets, string(key))

 // Release all bucket pages to freelist.
 child.nodes = nil
 child.rootNode = nil
 child.free()

 // Delete the node if we have a matching key.
 c.node().del(key)

 return nil
}

// del removes a key from the node.
func (n *node) del(key []byte) {
 node的del()方法具体实现参考之前3.3节del(k)
}

3.5 key&value 的插入、获取、删除

上面一节我们介绍了一下如何创建一个 Bucket、如何获取一个 Bucket。有了 Bucket,我们就可以对我们最关心的 key/value 键值对进行增删改查了。其实本质上,对 key/value 的所有操作最终都要表现在底层的 node 上。因为 node 节点就是用来存储真实数据的。

3.5.1 插入一个 key&value

// Put sets the value for a key in the bucket.
// If the key exist then its previous value will be overwritten.
// Supplied value must remain valid for the life of the transaction.
// Returns an error if the bucket was created from a read-only transaction,
// if the key is blank, if the key is too large, or if the value is too large.
func (b *Bucket) Put(key []byte, value []byte) error {
 if b.tx.db == nil {
  return ErrTxClosed
 } else if !b.Writable() {
  return ErrTxNotWritable
 } else if len(key) == 0 {
  return ErrKeyRequired
 } else if len(key) > MaxKeySize {
  return ErrKeyTooLarge
 } else if int64(len(value)) > MaxValueSize {
  return ErrValueTooLarge
 }

 // Move cursor to correct position.
 c := b.Cursor()
 k, _, flags := c.seek(key)

 // Return an error if there is an existing key with a bucket value.
 if bytes.Equal(key, k) && (flags&bucketLeafFlag) != 0 {
  return ErrIncompatibleValue
 }

 // Insert into node.
 key = cloneBytes(key)
 c.node().put(key, key, value, 0, 0)

 return nil
}

3.5.2 获取一个 key&value

// Get retrieves the value for a key in the bucket.
// Returns a nil value if the key does not exist or if the key is a nested bucket.
// The returned value is only valid for the life of the transaction.
func (b *Bucket) Get(key []byte) []byte {
 k, v, flags := b.Cursor().seek(key)
 // Return nil if this is a bucket.
 if (flags & bucketLeafFlag) != 0 {
  return nil
 }
 // If our target node isn't the same key as what's passed in then return nil.
 if !bytes.Equal(key, k) {
  return nil
 }
 return v
}

3.5.3 删除一个 key&value

// Delete removes a key from the bucket.
// If the key does not exist then nothing is done and a nil error is returned.
// Returns an error if the bucket was created from a read-only transaction.
func (b *Bucket) Delete(key []byte) error {
 if b.tx.db == nil {
  return ErrTxClosed
 } else if !b.Writable() {
  return ErrTxNotWritable
 }
 // Move cursor to correct position.
 c := b.Cursor()
 _, _, flags := c.seek(key)

 // Return an error if there is already existing bucket value.
 if (flags & bucketLeafFlag) != 0 {
  return ErrIncompatibleValue
 }
 // Delete the node if we have a matching key.
 c.node().del(key)

 return nil
}

3.5.4 遍历 Bucket 中所有的 key&value

// ForEach executes a function for each key/value pair in a bucket.
// If the provided function returns an error then the iteration is stopped and
// the error is returned to the caller. The provided function must not modify
// the bucket; this will result in undefined behavior.
func (b *Bucket) ForEach(fn func(k, v []byte) error) error {
 if b.tx.db == nil {
  return ErrTxClosed
 }
 c := b.Cursor()
 // 遍历键值对
 for k, v := c.First(); k != nil; k, v = c.Next() {
  if err := fn(k, v); err != nil {
   return err
  }
 }
 return nil
}

3.6 Bucket 的页分裂、页合并

关于本部分的内容过长,大家可以点击此处或者点击此处 进行阅读。

3.7 总结

本章我们主要介绍了 boltdb 中比较核心的两个数据结构:Bucket、node。为什么这两个数据结构放在一起介绍呢?答案是在 boltdb 中一个 Bucket 就对应一颗 b+树。而 b+树的结构(根节点、叶子节点、非叶子节点)、组织都是通过 node 来完成的。这也是为什么把他们放在一起介绍的主要原因。

在介绍中,我们主要围绕 Bucket 的创建、获取、删除、遍历、增删改 kv等操作进行展开。其次在遍历时,就引入了 Cursor 数据结构,一个 Bucket 对象(一颗 b+树)的遍历在 boltdb 中时通过一个栈来维护遍历的路径来完成的。这也是 Cursor 中stack的意义。

其次 Bucket 中对 kv 的操作都反应到底层的 node 上,因此我们又同时介绍了 node 的相关方法,例如put、get、del、spill、rebalance。

最后到此为止,我们的数据时如何存储的、组织的。以及内存和磁盘数据时如何转换映射的,我们就清楚了。下章将介绍 boltdb 中的事务了。有了事务我们的数据库才称得上是一个完备的数据库。

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69946831/viewspace-3143668/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2019-09-09

  • 博文量
    2646
  • 访问量
    42190