node
node是一个page在内存中的序列化表示; 对于B+树的相关操作,页面分裂,页面重平衡(rebalance)等都是通过node映射的
type node struct {
//当前node(page)所属于的Bucket,Bucket相当于关系型数据库中的Table
bucket *Bucket
//当前节点是否是叶子节点
isLeaf bool
//当前节点是否重新rebalance
unbalanced bool
//当前节点是否进行分裂
spilled bool
key []byte //node.inodes[0].key 子节点最开始的数据的key, 每个page的第一个key(最小的那个)
//当前页面对应的页面id
pgid pgid
//当前节点的副节点
parent *node
//当前节点的子节点
children nodes
//当前节点(页面)所世纪存储的k/v数据(如果是叶子节点)
inodes inodes
}
inode
inode表示一个node内部的一个实际节点,它能够实际指向一个元素(k/v对),也能指向一个 还没有被写入到页面的一个k/v对。
type inode struct {
flags uint32 //当前inode的flage, 当前只有bucketLeafFlag //todo
pgid pgid //当前对应的page_id,如果还没有写入页面则为0
key []byte //实际的key
value []byte //实际的value
}
BoltDB的数据通过页面构成B+树,所有的数据都存储在叶子节点,而node是是page在内存中的序列化表示, 因此对于B+树的操作,都是先通过定位到具体的page,通过将page序列化成node,通过对相关node的操作完成对 B+树相关的操作。 因此node支持的操作有:
//返回当前B+树的root节点
func (n *node) root() *node {
if n.parent == nil {
return n
}
return n.parent.root()
}
//返回一个page至少应该有多少个key, 非叶子节点至少需要两个key
func (n *node) minKeys() int {
if n.isLeaf {
return 1
}
return 2
}
//返回当前node(page)的大小
//页面的大小等于 pageHeader + 对象头 * 对象数量 + 实际的key + 实际的value的总数
// https://juejin.cn/post/6942016731019739166
func (n *node) size() int {
sz, elsz := pageHeaderSize, n.pageElementSize()
for i := 0; i < len(n.inodes); i++ {
item := &n.inodes[i]
sz += elsz + len(item.key) + len(item.value)
}
return sz
}
//返回当前节点第i个子节点对应的node(page)
//page中的数据按照key升序排列
func (n *node) childAt(index int) *node {
if n.isLeaf {
panic(fmt.Sprintf("invalid childAt(%d) on a leaf node", index))
}
return n.bucket.node(n.inodes[index].pgid, n)
}
//返回当前节点的右边兄弟节点和左边兄弟节点
func (n *node) nextSibling() *node {
if n.parent == nil {
return nil
}
index := n.parent.childIndex(n)
if index >= n.parent.numChildren()-1 {
return nil
}
return n.parent.childAt(index + 1)
}
func (n *node) prevSibling() *node {
if n.parent == nil {
return nil
}
index := n.parent.childIndex(n)
if index == 0 {
return nil
}
return n.parent.childAt(index - 1)
}
//put操作将一个key/value写入node中,通过key找到在node中的位置,如果olekey=newkey,则替换value,
//否则插入一个新的inode
func (n *node) put(oldKey, newKey, value []byte, pgid pgid, flags uint32) {
if pgid >= n.bucket.tx.meta.pgid {
//todo 这个判断
panic(fmt.Sprintf("pgid (%d) above high water mark (%d)", pgid, n.bucket.tx.meta.pgid))
}
// 找到newkey的插入位置
index := sort.Search(len(n.inodes), func(i int) bool { return bytes.Compare(n.inodes[i].key, oldKey) != -1 })
// 判断是否是替换插入
exact := len(n.inodes) > 0 && index < len(n.inodes) && bytes.Equal(n.inodes[index].key, oldKey)
if !exact {
//原来key不存在,则新建一个节点插入
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
}
func (n *node) del(key []byte) {
//通过key找到第一个不大于key的index位置
index := sort.Search(len(n.inodes), func(i int) bool { return bytes.Compare(n.inodes[i].key, key) != -1 })
//如果当前key不存在则返回
if index >= len(n.inodes) || !bytes.Equal(n.inodes[index].key, key) {
return
}
//如果存在则删除相关ninode
n.inodes = append(n.inodes[:index], n.inodes[index+1:]...)
//标记当前node是没有进行重新平衡的(rebalance的过程是将多个小页面合并成一个更大页面的过程)
//元素删除才会触发rebalance的过程
n.unbalanced = true
}
//read将一个page的内容反序列化到一个内存的node中
func (n *node) read(p *page) {
n.pgid = p.id
n.isLeaf = (p.flags & leafPageFlag) != 0
n.inodes = make(inodes, int(p.count)) //根据page中元素的个数,分配相应的inode空间
for i := 0; i < int(p.count); i++ {
//将pageElement对应的数据反序列化到inode中
inode := &n.inodes[i]
if n.isLeaf {
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()
}
}
//如果当前node有至少一个inode,则node.key等于第一个inode的key
if len(n.inodes) > 0 {
n.key = n.inodes[0].key
} else {
n.key = nil
}
}
//write将node中的item写入一个或者多个page
//如果当前node一个page的容量无法承载,则会分配多个连续的page来存储当前的node
func (n *node) write(p *page) {
if n.isLeaf {
//根据当前是否是叶子节点来标记page的flag
p.flags |= leafPageFlag
} else {
p.flags |= branchPageFlag
}
//最多只能65535个inode
if len(n.inodes) >= 0xFFFF {
panic(fmt.Sprintf("inode overflow: %d (pgid=%d)", len(n.inodes), p.id))
}
p.count = uint16(len(n.inodes))
//如果当前的node不包含元素,返回
if p.count == 0 {
return
}
//根据pageElementSize*lengthOfInode找到在page中的写入偏移量
b := (*[maxAllocSize]byte)(unsafe.Pointer(&p.ptr))[n.pageElementSize()*len(n.inodes):]
for i, item := range n.inodes {
//遍历node中的inode,将其写入page中,写入pageHeader(leafElement/branchElement)
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
}
//如果分配的内存的数量小于key+value的实际长度,需要重新分配
klen, vlen := len(item.key), len(item.value
if len(b) < klen+vlen {
b = (*[maxAllocSize]byte)(unsafe.Pointer(&b[0]))[:]
}
//将实际的key+value写入page中
copy(b[0:], item.key)
b = b[klen:]
copy(b[0:], item.value)
b = b[vlen:]
}
}
//B+树中,节点分裂
//split根据传入的pageSize将指定node分裂成多个node
func (n *node) split(pageSize int) []*node {
var nodes []*node
node := n
for {
//将当前节点分裂成a、b两个node
a, b := node.splitTwo(pageSize)
nodes = append(nodes, a)
if b == nil {
break
}
//递归的分裂b节点,直到b节点为nil,则所有的节点的大小都不大于pageSize
node = b
}
return nodes
}
//splitTwo将给定节点按照pageSize大小分裂成两个更小的节点
func (n *node) splitTwo(pageSize int) (*node, *node) {
//如果没有达到分裂的调节直接返回,不分裂
//分裂的条件为:叶子节点至少需要两个inode,非叶子节点至少有一个inode,并且节点的大小不应该小于pageSize
if len(n.inodes) <= (minKeysPerPage*2) || n.sizeLessThan(pageSize) {
return n, nil
}
var fillPercent = n.bucket.FillPercent
if fillPercent < minFillPercent {
fillPercent = minFillPercent
} else if fillPercent > maxFillPercent {
fillPercent = maxFillPercent
}
//根据填充因子决定分裂的阈值
threshold := int(float64(pageSize) * fillPercent)
//根据阈值计算分裂的inodes数据的index,根据inodes的index将inodes数组分裂成为两部分
splitIndex, _ := n.splitIndex(threshold)
//如果当前节点没有父节点,需要创建一个
if n.parent == nil {
n.parent = &node{bucket: n.bucket, children: []*node{n}}
}
//创建一个新的节点,将分裂后的节点加入到当前父节点的子节点中,新产生的node是没有分配页面的,因此pageID=0
next := &node{bucket: n.bucket, isLeaf: n.isLeaf, parent: n.parent}
n.parent.children = append(n.parent.children, next)
//将当前节点的inodes根据splitIndex分裂成两部分
next.inodes = n.inodes[splitIndex:]
n.inodes = n.inodes[:splitIndex]
n.bucket.tx.stats.Split++
return n, next
}
//splitIndex根据传入的阈值的大小,计算出应该分裂的inodes数组的index
func (n *node) splitIndex(threshold int) (index, sz int) {
sz = pageHeaderSize //pageHeader的大小
for i := 0; i < len(n.inodes)-minKeysPerPage; i++ {
index = i
inode := n.inodes[i]
//每个inode的实际大小包含三部分: elementHeader.Size+key.Size+value.Size三部分的大小
elsize := n.pageElementSize() + len(inode.key) + len(inode.value)
if i >= minKeysPerPage && sz+elsize > threshold {
//当前满足前面i个inode的总的大小大于阈值,从当前index分裂
break
}
sz += elsize
}
return
}
//spill递归的将node所包含的所有节点(叶子节点)写入脏页,并且在这个过程中会产生节点的分裂。
//如果无法分配脏页的内存则返回错误
//spill描述了事务提交过程中b+树的分裂过程,由于b+树的页面在内存中是通过node表示的,因此对于页面的分裂也是通过node进行映射的
func (n *node) spill() error {
var tx = n.bucket.tx
if n.spilled { //如果当前节点正在进行分裂,则返回;同时一个页面只能进行一次分裂,不能多个分裂操作并行
return nil
}
//将当前的叶子节点按照key进行排序
sort.Sort(n.children)
//先递归的进行叶子节点的分裂
for i := 0; i < len(n.children); i++ {
if err := n.children[i].spill(); err != nil {
return err
}
}
//当前不在需要叶子节点,叶子节点列表仅仅是用来追踪节点的分裂
n.children = nil
//将当前节点按照pageSize分裂成多个节点
var nodes = n.split(tx.db.pageSize)
for _, node := range nodes {
if node.pgid > 0 {
//如果原来有旧的页面,split之后需要把旧的页面释放归还,放到freelist中
//由于node分裂产生新的node,在这个过程中node还没有序列化到对应的page, 因此对应的page.id=0
tx.db.freelist.free(tx.meta.txid, tx.page(node.pgid))
node.pgid = 0
}
//新分片一个页面来存放当前的node,如果分配失败,则直接返回
p, err := tx.allocate((node.size() / tx.db.pageSize) + 1)
if err != nil {
return err
}
// 如果分配的页面的id大于混存的pageId,则是吧(todo ??? tx.Commit pageID的变更)
if p.id >= tx.meta.pgid {
panic(fmt.Sprintf("pgid (%d) above high water mark (%d)", p.id, tx.meta.pgid))
}
//将node反序列化到新分片的page
node.pgid = p.id
node.write(p)
node.spilled = true
//如果当前node的parent节点不为空,则将当前节点的key(或者是第一个inode的key写入parent的node中)
if node.parent != nil {
var key = node.key
if key == nil {
key = node.inodes[0].key
}
//如果存在则更新,否则插入
node.parent.put(key, node.inodes[0].key, nil, node.pgid, 0)
node.key = node.inodes[0].key
}
tx.stats.Spill++
}
//如果是root node进行分裂,则需要新创建一个root node,并且重新进行分裂
if n.parent != nil && n.parent.pgid == 0 {
//清空当前的叶子节点信息,保证不会再次进行分裂
n.children = nil
return n.parent.spill()
}
return nil
}
//rebalance将多个兄弟节点组合成一个节点,如果他们的大小小于填充阈值或者inode的个数小于最小数量
func (n *node) rebalance() {
if !n.unbalanced { //当前正在进行rebalance
return
}
n.unbalanced = false //标识正在进行rebalance
//更新统计数据
n.bucket.tx.stats.Rebalance++
//计算填充阈值,大小为页面大小的四分之一
var threshold = n.bucket.tx.db.pageSize / 4
//一个页面至少要四分之一个page的大小并且至少要有2个(叶子节点)或者1个(非叶子节点)key
if n.size() > threshold && len(n.inodes) > n.minKeys() {
return
}
//当前节点是根节点,特殊处理
if n.parent == nil {
//当前节点是branch节点并且只有一个一个inode, 分裂当前节点
if !n.isLeaf && len(n.inodes) == 1 {
// 将root节点的叶子节点上移
// 创建一个新的子节点,以当前节点作为root节点
child := n.bucket.node(n.inodes[0].pgid, n)
n.isLeaf = child.isLeaf
n.inodes = child.inodes[:]
n.children = child.children
//重新计算当前叶子节点的parent //todo
for _, inode := range n.inodes {
//当前节点缓存在bucket中
if child, ok := n.bucket.nodes[inode.pgid]; ok {
child.parent = n
}
}
//删除老得叶子节点
child.parent = nil
delete(n.bucket.nodes, child.pgid)
child.free()
}
return
}
//如果当前的节点没有存储key ,移除当前的节点
if n.numChildren() == 0 {
//如果当前的节点没有叶子节点,并行size<threshold
//移除当前这个节点
n.parent.del(n.key) //从parent.inodes中删除当前这个node
n.parent.removeChild(n) //从parent.children移除当前数组
delete(n.bucket.nodes, n.pgid)
n.free() //释放当前节点对应的page
n.parent.rebalance() //递归对父节点进行rebalance
return
}
//下面的情况是当前节点有数据
var target *node //找到需要rebalance的节点的位置
var useNextSibling = n.parent.childIndex(n) == 0 //找到当前节点在父节点中的位置,是否是最左边的节点
if useNextSibling {//当前节点是最左边的节点
//右边的兄弟节点
target = n.nextSibling() //找到当前节点的右边的兄弟节点
} else {
//左边的兄弟节点
target = n.prevSibling()
}
// 如果当前节点和target节点都太小了,则合并他们
if useNextSibling {//如果目标节点是当前节点的右边的兄弟节点,则将target节点合并到当前节点,
//遍历target节点的元素,重新计算其父节点
for _, inode := range target.inodes {
//如果当前的bucket缓存了inode对应的pageid的页面,
if child, ok := n.bucket.nodes[inode.pgid]; ok {
//将child节点从其父节点中移除
child.parent.removeChild(child)
child.parent = n //重新计算其父节点为当前节点
//将child加入当前node的子节点中
child.parent.children = append(child.parent.children, child) // ==> n.children = append(n.children, child) equality ?
}
}
// 将目标节点的元素添加到当前节点的元素数组中
n.inodes = append(n.inodes, target.inodes...)
n.parent.del(target.key) //将目标节点的key从父节点中移除(target节点和n的父节点是同一个)
n.parent.removeChild(target) //从目标节点的父节点的叶子节点中移除目标节点
delete(n.bucket.nodes, target.pgid) //删除当前bucket的节点缓存中的目标节点
target.free() //释放target节点占有的页面
} else {
//如果target节点是当前节点的左边的兄弟节点,则将当前节点合并到左边的兄弟节点
for _, inode := range n.inodes {
if child, ok := n.bucket.nodes[inode.pgid]; ok {
child.parent.removeChild(child)
child.parent = target
child.parent.children = append(child.parent.children, child)
}
}
//将当前节点重父节点和当前bucket的缓存中移除,并且将当前节点的元素添加到左边的兄弟节点中
target.inodes = append(target.inodes, n.inodes...) // inodes按照key排序,添加到目标节点中仍然是有序的
n.parent.del(n.key)
n.parent.removeChild(n)
delete(n.bucket.nodes, n.pgid)
n.free()
}
// 递归的合并父节点
n.parent.rebalance()
}