小编在看etcd存储(store)模块的时候,发现它在进行key和keyIndex转换的时候,用到了btree包(http://godoc.org/github.com/google/btree)。btree是Google开源的一个Go语言的BTree实现,整个代码不到1000行,实现的非常简练,组织分层也做的很好,并对gc和并发读写做了很多优化,值得一读。小编打算用两篇文章讲解BTree内容,本文上篇主要介绍实现原理,下篇主要介绍btree源码实现。
BTree定义
BTree和B-Tree都是指B树,不要把B-Tree理解成了B-树。关于BTree在《计算机程序设计与艺术》和《算法导论》对其描述是不一样的。一种是通过"阶"的概念来定义的,另一种是通过"度"的概念定义的,两者描述的本质是一样的。
我们先来看通过阶的概念的定义,阶指一个节点的最大子树的个数,定义如下:
❝树中每个节点至多有m颗子树 若根节点不是叶子节点,则至少有两颗子树 除根节点之外的所有非终端节点至少有「m/2」颗子树 所有的非终端节点中包含关键字和指向子树根节点的指针 所有叶子节点在同一个层次上 ❞
下面是算法导论中通过度来定义BTree, 度是一个节点子树的个数,对于一颗最小度为t(t>=2)的BTree,节点中的关键字为key,有如下限制:
❝除了根节点之外,其他节点的t-1<=len(key)<=2t-1 非叶子节点的子树个数t<=len(children)<=2t ❞
两者有点细微的差别,根据《计算机程序设计艺术》的定义,最小的树为3阶BTree,即树的非叶子节点的子树的个数为2或3,根据《算法导论》的定义,最小的树为2度数,对应的前者的阶数为4,即树的非叶子节点个数可以为2、3或者4. etcd中用到的btree包是按这里的度的定义实现的。下图是一个度为3的BTree,除了叶子节点,每个节点的子树个数不是2个就是3个,0004节点的子树有2个,0047|0051节点的子树有3个。
BTree使用场景
BTree常用于实现数据库索引,例如在MongoDB中的索引是用BTree实现的,MySQL中的innodb存储引擎用B 树存储索引信息。通过前面的定义可以看到,BTree是一种平衡多路查找树,与AVL树和红黑树等二叉树比较起来,BTree通过多叉,降低了树的高度,从而减少了查询的次数。为啥数据库的索引采用BTree实现呢?因为数据库的索引信息以树形结构存放在磁盘上,对于高度为h的树,最多需要进行h次查找,对于存放在磁盘上的文件来说,需要读取磁盘h次,而读取磁盘的操作与操作内存相比是很慢的,一次磁盘读取耗时为寻道时间 旋转磁头时间 数据读取传输耗时。普通的机械硬盘,寻道时间大概在10毫秒左后,旋转磁头时间就是磁头移动到对应磁道后,旋转到对应扇区所需要的时间,可以看到整个磁盘IO操作是个很耗时的过程。而BTree降低了树的高度,减少了磁盘读取次数,所以数据库的索引采用BTree或B 树实现。
BTree实现原理
BTree的核心操作包含树的创建,树中节点的删除,元素的查找。下面分别分析每个操作的具体实现。
插入
插入操作是向BTree中插入一条记录,即向里面添加一个key-value键值对。如果要插入的key在BTree中已存在,则用当前的value更新之前的旧value.如果BTree中不存在这个key,则在它的叶子节点中进行插入操作。具体算法流程如下:
- 根据要插入的key的值,在BTree查找key存不存,如果已存在,用新的value覆盖旧的value,操作结束
- 如果流程1中,没有找到key,则定位到要插入的叶子节点并插入
- 判断2中刚插入节点key的个数是否满足BTree的性质,如果不满足,则执行下面的第4步操作
- 以插入节点中间的key为中心,分裂成左右两部分,然后将中间的key插入到它的父节点中,这个key的左子树指向分裂后的左半部分,右子树指向分裂后的右半部分。然后继续判断父节点满不满足BTree性质,如果不满足,继续对父节点进行分裂,否则流程执行结束
下面以度(Degree=3)BTree的创建过程为例,介绍插入过程。
向BTree中插入4,插入后只有一个key,因为这是首次插入。
向BTree中插入51,直接将51加入与4同节点中,此时该节点有2个key,满足每个节点不超过2个key的性质.
向BTree中插入38, 度为3的BTree,每个节点最多有2个key, 此时节点有3个key,不满足BTree性质,将中间的key提升到父节点中,调整之后符合BTree定义,插入操作结束。
向BTree中插入43,添加到叶子节点51所在的位置。
向BTree中插入48,添加48到43|51所在的节点后,此时该节点不满足BTree性质,对其进行拆分,将中间的48加入到父节点(38所在的节点),43|48|51节点中的key被分成43和51两部分,48的左右子树分别指向它。
向BTree中插入1
向BTree中插入10,此时1|4|10节点不满足BTree性质,需要进行分裂,将4插入到父节点中,插入之后,父节点4|30|48也不满足BTree性质,继续对其进行分裂。
插入的核心就是当节点的key的数量不满足BTree性质时,向上进行分裂,即将当前节点的中间key插入到父节点中,这样能够确保分裂之后节点的层次深度保持不变。如果父节点也不满足BTree性质,继续对其分裂,这样一路向上,直到根节点,保证整颗树