boltdb node详解

763 阅读7分钟

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