前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >boltdb源码分析系列-迭代器

boltdb源码分析系列-迭代器

作者头像
数据小冰
发布2022-08-15 15:03:48
4630
发布2022-08-15 15:03:48
举报
文章被收录于专栏:数据小冰

Cursor是boltdb中的迭代器,它能够按顺序访问Bucket中的数据。在前面的文章中说过,一个Bucket是一颗B+Tree. Cursor任务是对B+Tree中节点中数据的访问,因为B+Tree树中的数据是保存在叶子节点,所以严格来说Cursor遍历访问的是叶子节点中的存储的数据。

Cursor是什么

前文对Cursor进行了概括说明,本小节将结合Cursor的数据结构定义,对其进行更详细的说明。

Cursor是boltdb中的一个结构体,定义在cursor.go文件中。它只有2个字段,如下所示。

  • bucket:Cursor关联的桶,Cursor的核心工作是对桶中的数据进行顺序访问,bucket记录的就是要访问的桶
  • stack:Bucket是一颗B+Tree,也就是一颗多叉树,Cursor在访问的时候是按照二叉树那种先序遍历的方式进行的,访问到的每个节点都会加入到stack中。所以它是切片结构,存储的元素是elemRef复合类型。elemRef中两个指针和一个整数字段。page和node是一一对应的,它们都描述的是B+Tree节点中的node和page信息,index用于表示当前遍历的是节点中的第几个元素。

一颗B+Tree包含多个节点,而每个节点中又装有很多数据。所以这是一个二维结构,定位一个数据,需要先定位到某个节点,在定位到节点中的第几个元素。通过下标索引访问stack锁定到某个节点(elemRef),在通过elemRef中的index便可锁定到具体的元素。

代码语言:javascript
复制
type Cursor struct {
 bucket *Bucket
 stack  []elemRef
}

type elemRef struct {
 page  *page
 node  *node
 index int
}

为什么需要Cursor

在说需要Cursor原因之前,我们先来看boltdb中B+Tree的结构。标准的B+Tree结构如下图所示,像MySQL中索引组织也是B+Tree,它的结构就是下面这样的。

所有的数据都存储在叶子节点上,对应到图中的绿色内容。图中一共有9个叶子节点,它们之间通过指针连接了起来,构成了一个链表结构。

而boltdb中B+Tree的叶子节点并没有通过链表连接起来,这样做的原因小编觉得是为了减少了维持B+Tree平衡性质的难度,但在遍历的时候增加了一定的困难。所以这里抽象了Cursor结构来进行遍历,在遍历的时候记下了遍历的路径,方便进行快速回溯。

遍历路径上的每步是elemRef结构,在前面Cursor结构体定义介绍中有说明。这里在强调下处理时node和page的差别,处理时优先使用node,否则使用page。因为node中的数据可能是更新过的,page中的数据来自mmap,是没有被更新的。

总结起来,因为boltdb中的B+Tree没有将叶子节点通过链表串联起来,为了能够方便对其访问,抽象处理了Cursor迭代器,来对其进行遍历操作。

Cursor工作原理

Cursor的功能是对B+Tree的遍历访问,提供了First、Last、Next、Prev和Seek等方法,分别是查找第一个数据,最后一个数据,下一个数据,前一个数据以及定位到某个位置。

First、Last、Seek方法查询的是一个绝对位置,也就是说,调用它们不会受到当前Cursor游标位置的影响,都是从根节点开始查询。体现在代码中是它们都有重置c.stack = c.stack[:0]操作,

Next、Prev查询的是相对于当前游标位置来说的,所以调用这两个方法,依赖于调用前游标所在的位置。它们实现中不会有重置c.stack操作。

为了代码复用,抽取了first、last、next、seek等内部方法,供对外提供方法的接口First、Last、Next、Prev和Seek调用。

获取第一个数据

First方法返回B+Tree中第一个数据,就是最左侧(如果里面元素非空)的叶子节点的第一个元素。处理逻辑概括起来分为3步:

  1. 加载根节点到迭代器stack
  2. 调用c.first方法,一路向下向左,走到叶子节点, 并将路径上的节点加入stack
  3. 取stack最后一个元素(目标叶子节点)它里面的第一个数据
代码语言:javascript
复制
func (c *Cursor) First() (key []byte, value []byte) {
 _assert(c.bucket.tx.db != nil, "tx closed")
 // 重置stack
 c.stack = c.stack[:0]
 // 加载根节点
 p, n := c.bucket.pageNode(c.bucket.root)
 // 根节点信息加入stack
 c.stack = append(c.stack, elemRef{page: p, node: n, index: 0})
 // 一路向下走到第一个叶子节点,内部通过for循环走到最左侧的叶子节点
 c.first()

 // https://github.com/boltdb/bolt/issues/450
 // 叶子节点中没有元素,移动到下一个叶子节点
 if c.stack[len(c.stack)-1].count() == 0 {
  c.next()
 }
    // 获取最左侧叶子节点中的第一个key-value
 k, v, flags := c.keyValue()
 // 如果是一个桶节点,value为空
 if (flags & uint32(bucketLeafFlag)) != 0 {
  return k, nil
 }
 // 存储的是数据,返回key-value
 return k, v

}
获取最后一个数据

Last方法获取B+Tree中最后一个数据,处理过程与First类型,只不过在查找的时候一路向下向右走,定位到最右侧的叶子节点。具体处理过程也分为3步,与First类似,这里不在展开说明了。

代码语言:javascript
复制
func (c *Cursor) Last() (key []byte, value []byte) {
 _assert(c.bucket.tx.db != nil, "tx closed")
 c.stack = c.stack[:0]
 // 加载根节点
 p, n := c.bucket.pageNode(c.bucket.root)
 ref := elemRef{page: p, node: n}
 // 设置index为最后一个元素位置
 ref.index = ref.count() - 1
 // 加入路径缓存stack
 c.stack = append(c.stack, ref)
 // 一路向下走到最右侧的节点
 c.last()
 // 获取游标位置(最右边节点的最后一个)元素
 k, v, flags := c.keyValue()
 // 如果是Bucket,返回value为空
 if (flags & uint32(bucketLeafFlag)) != 0 {
  return k, nil
 }
 // 返回key-value
 return k, v
}
获取下一个数据

Next方法获取下一个数据,是基于当前游标位置的下一个位置数据,实际调用的是内部的next方法,next方法分析见下文。

代码语言:javascript
复制
func (c *Cursor) Next() (key []byte, value []byte) {
 _assert(c.bucket.tx.db != nil, "tx closed")
 k, v, flags := c.next()
 if (flags & uint32(bucketLeafFlag)) != 0 {
  return k, nil
 }
 return k, v
}

next方法真正实现将游标移动到下一个位置,并返回它的数据。有两种情况需要考虑:

  • 情况1:当前游标index在节点(叶子节点)元素的内部,即index还没有走到节点中的最后一个元素位置,这种很好处理,就是index++即可
  • 情况2:当前游标index已经走到节点中的最后一个元素位置,需要将游标移动到当前节点的下一个邻近节点的第一个元素位置。因为boltdb中B+Tree叶子节点没有构成链表结构,所以需要回溯到当前节点的父节点,然后走到它的邻近右节点。

情况1的处理在下面for循环中的elem.index < elem.count()-1,直接elem.index++即游走到了下一个元素位置。此种情况,c.stack = c.stack[:i+1]会保持不变,调用c.first()会立即返回,因为当前已在叶子节点。

情况2的处理也在for循环中,for循环会执行两次,情况1中for循环只会执行一次就break掉了。循环完之后,执行c.stack = c.stack[:i+1]会切掉右边数据,相当于剪掉了B+Tree已处理过的节点,这个过程就是回溯。然后调用c.first()定位到它的邻近叶子节点。

代码语言:javascript
复制
func (c *Cursor) next() (key []byte, value []byte, flags uint32) {
 for {
  var i int
  // 切片c.stack中保存的是访问的前缀信息,i从大到小循环,先处理游标最后位置
  // 先在节点内部找到下一个元素,如果节点内部元素已全部访问完,i--走到下一个节点
  for i = len(c.stack) - 1; i >= 0; i-- {
   elem := &c.stack[i]
   // 移动到节点中的下一个元素
   if elem.index < elem.count()-1 {
    elem.index++
    break
   }
  }

  // i为-1说明整颗树已经处理完了
  if i == -1 {
   return nil, nil, 0
  }

  // 清理掉已经处理过的节点,保留剩下未访问处理的
  c.stack = c.stack[:i+1]
  // 定位到剩下未处理完成的节点,c.stack[i]即为待处理的节点
  // c.stack[i].index已经定位到节点中位置
  c.first()

  // https://github.com/boltdb/bolt/issues/450
  // 节点中没有数据,跳出
  if c.stack[len(c.stack)-1].count() == 0 {
   continue
  }
  // 返回key-value
  return c.keyValue()
 }
}
获取前一个数据

Prev方法是Next方法的反过程,与Next方法类似,也有两种情况需要考虑,分别是当前游标位置处于节点中的内部,不是节点的第一个元素位置,和已处于第一个运算位置。

  • 情况1:当前游标位置不在节点的中第一个元素位置,处理很简单,将index减一即可
  • 情况2:这个时候要找到当前节点的左邻近叶子节点,将index移动左邻近叶子节点的最后一个元素位置。

代码中的for循环,融合处理了上面两种情况,当循环结束的时候,调用c.last()方法定位到游标节点的最后一个元素位置。

代码语言:javascript
复制
func (c *Cursor) Prev() (key []byte, value []byte) {
 _assert(c.bucket.tx.db != nil, "tx closed")

 // 现在当前节点内部移动到前一个元素,特殊情况,当前已经位于节点的第一个元素位置
 // 这时需要找到当前节点的前驱叶子节点
 for i := len(c.stack) - 1; i >= 0; i-- {
  elem := &c.stack[i]
  if elem.index > 0 {
   elem.index--
   break
  }
  // 调整stack,c.stack[i]为当前活动的节点
  c.stack = c.stack[:i]
 }

 // 已经遍历完整颗树
 if len(c.stack) == 0 {
  return nil, nil
 }

 // 调用c.last定位到c.stack游标位置节点的最后一个节点的最后一个元素
 c.last()
 k, v, flags := c.keyValue()
 // 是桶,value为nil
 if (flags & uint32(bucketLeafFlag)) != 0 {
  return k, nil
 }
 return k, v
}
迭代器定位到给定key的位置

Seek方法将迭代器定位到给定key的位置,并返回key-value值。如果key不存在,迭代器会移动到下一个数据位置。处理的核心调用了内部的seek方法,下面分析这个处理流程。

代码语言:javascript
复制
func (c *Cursor) Seek(seek []byte) (key []byte, value []byte) {
 k, v, flags := c.seek(seek)

 if ref := &c.stack[len(c.stack)-1]; ref.index >= ref.count() {
  k, v, flags = c.next()
 }

 if k == nil {
  return nil, nil
 } else if (flags & uint32(bucketLeafFlag)) != 0 {
  return k, nil
 }
 return k, v
}

seek方法调用search方法完成key的定位,即查找key所在的位置,因为B+Tree中数据有序,search的时候采用二分查找sort.Search实现快速定位。

查询流程是一个递归的过程,首先将根节点加载到stack中。递归的出口是走到了叶子节点,并查询给定的key在叶子节点中是否存在。如果当前是非叶子节点,优先从node中查找,如果node为空,则从page中查找。它们的内部也都会又调用search方法,所以形成了两条隐含的递归过程。

代码语言:javascript
复制
search-->searchNode-->search

serach-->searchPage-->search

searchNode和searchPage处理过程非常类似,我觉得源码实现有点重复,可以提取复用😁,处理过程非常简单,这里不再分析。

代码语言:javascript
复制
func (c *Cursor) seek(seek []byte) (key []byte, value []byte, flags uint32) {
 _assert(c.bucket.tx.db != nil, "tx closed")

 // 清理stack
 c.stack = c.stack[:0]
 // 从根节点开始查找
 c.search(seek, c.bucket.root)
 ref := &c.stack[len(c.stack)-1]

 // key不存在
 if ref.index >= ref.count() {
  return nil, nil, 0
 }

 return c.keyValue()
}

func (c *Cursor) search(key []byte, pgid pgid) {
 p, n := c.bucket.pageNode(pgid)
 if p != nil && (p.flags&(branchPageFlag|leafPageFlag)) == 0 {
  panic(fmt.Sprintf("invalid page type: %d: %x", p.id, p.flags))
 }
 e := elemRef{page: p, node: n}
 c.stack = append(c.stack, e)

 // e是叶子节点
 if e.isLeaf() {
  // 在叶子节点中查找key
  c.nsearch(key)
  return
 }

 if n != nil {
  // 先从node中查找
  c.searchNode(key, n)
  return
 }
 // 从page中查找
 c.searchPage(key, p)
}

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Cursor是什么
  • 为什么需要Cursor
  • Cursor工作原理
    • 获取第一个数据
      • 获取最后一个数据
        • 获取下一个数据
          • 获取前一个数据
            • 迭代器定位到给定key的位置
            相关产品与服务
            云数据库 MySQL
            腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档