从零实现一个数据库(DataBase) Go语言实现版 4.B树实现(Part1))

news2024/11/23 1:54:30

英文源地址
本章将使用Go语言实现一个不可变地B+树.这是一个最小实现, 因此很容易理解.

Node节点的格式

我们的B树最终将被持久化到磁盘上, 因此我们首先需要为b树节点设计数据格式.如果没有这种格式, 我们将无法知道节点的大小以及何时拆分节点.
一个节点包含:

  • 一个固定大小的头部, 包含节点的类型(叶子节点或内部节点)和键的数量
  • 指向子节点的指针列表(内部节点使用)
  • 指向每个键值对的偏移量列表
  • 封装的KV对
    在这里插入图片描述
    以下是KV键值对的格式.长度后紧跟着数据.
    在这里插入图片描述
    为了简单起见, 叶子节点和内部节点都使用相同的格式.

数据类型

既然我们最终要将b树转存至磁盘, 为什么不使用字节数组作为我们的内存数据结构呢?

type BNode struct {
	data []byte
}

const (
	BNODE_NODE = 1
	BNODE_LEAF = 2
)

我们不能使用内存指针, 这些指针是64位整数, 引用磁盘页而不是内存节点.我们将添加一些回调函数来抽象这个方面, 以便我们的数据结构仍然是纯粹的数据结构代码.

type BTree struct {
	root uint64

	get func(uint64) BNode
	new func(BNode) uint64
	del func(uint64)
}

页大小定义为4K字节.更大的页面大小(如8K或16K)也可以.
我们还对键和值的大小做了一些约束.所以只有一个kv键值对的节点总是可以放在一个页上.如果需要支持更大的键或更大的值, 就必须为它们分配额外的页, 这会增加复杂度.

const HEADER = 4

const BTREE_PAGE_SIZE = 4096
const BTREE_MAX_KEY_SIZE = 1000
const BTREE_MAX_VAL_SIZE = 3000

func init() {
	node1max := HEADER + 8 + 2 + 4 + BTREE_MAX_KEY_SIZE + BTREE_MAX_VAL_SIZE
	assert(node1max <= BTREE_PAGE_SIZE)
}

解码b树节点

由于节点只是一个字节数组, 因此我们将添加一些辅助函数来访问其内容.

func (node BNode) btype() uint16 {
	return binary.LittleEndian.Uint16(node.data)
}

func (node BNode) nkeys() uint16 {
	return binary.LittleEndian.Uint16(node.data[2:4])
}

func (node BNode) setHeader(btype uint16, nkeys uint16) {
	binary.LittleEndian.PutUint16(node.data[0:2], btype)
	binary.LittleEndian.PutUint16(node.data[2:4], nkeys)
}

func (node BNode) getPtr(idx uint16) uint64 {
	assert(idx < node.nkeys())
	pos := HEADER + 8 * idx
	return binary.LittleEndian.Uint64(node.data[pos:])
}

func (node BNode) setPtr(idx uint16, val uint64) {
	assert(idx < node.nkeys())
	pos := HEADER + 8 * idx
	binary.LittleEndian.PutUint64(node.data[pos:], val)
}

关于偏移列表的一些细节

  • 偏移量与第一个kv对的位置有关
  • 第一个kv对的偏移量始终为零, 因此它不存储在列表里
  • 我们将到最后一个kv对的偏移量存储在偏移列表, 这用于确定节点的大小
func offsetPos(node BNode, idx uint16) uint16 {
	assert(1 <= idx && idx <= node.nkeys())
	return HEADER + 8 * node.nkeys() + 2 * (idx - 1)
}

func (node BNode) getOffset(idx uint16) uint16 {
	if idx == 0 {
		return 0
	}
	return binary.LittleEndian.Uint16(node.data[offsetPos(node, idx):])
}

func (node BNode) setOffset(idx uint16, offset uint16) {
	binary.LittleEndian.PutUint16(node.data[offsetPos(node, idx):], offset)
}

偏移列表用于快速定位第n个KV键值对

func (node BNode) kvPos(idx uint16) uint16 {
	assert(idx <= node.nkeys())
	return HEADER + 8 * node.nkeys() + 2 * node.nkeys() + node.getOffset(idx)
}

func (node BNode) getKey(idx uint16) []byte {
	assert(idx < node.keys())
	pos := node.kvPos(idx)
	klen := binary.LittleEndian.Uint16(node.data[pos:])
	return node.data[pos+4:][:klen]
}

func (node BNode) getVal(idx uint16) []byte {
	assert(idx < node.nkeys())
	pos := node.kvPos(idx)
	klen := binary.LittleEndian.Uint16(node.data[pos+0:])
	vlen := binary.LittleEndian.Uint16(node.data[pos+2:])
	return node.data[pos+4+klen:][:vlen]
}

并确定节点的大小

func (node BNode) nbytes() uint16 {
	return node.kvPos(node.nkeys())
}

b树插入操作

代码被分解成了小步骤.

查找key

要将键插入叶节点, 我们需要查找它在kv列表中的位置

func nodeLookupLE(node BNode, key []byte) uint16 {
	nkeys := node.nkeys()
	found := uint16(0)
	for i := uint16(1); i < nkeys; i++ {
		cmp := bytes.Compare(node.getKey(i), key)
		if cmp <= 0 {
			found = i
		}
		if cmp >= 0	{
			break
		}	
	}
	return found
}

查找对叶子节点和内部节点都有效, 注意, 第一个键将被跳过以进行比较, 因为它已经从父节点进行了比较.

更新叶子节点

在查找到要插入的位置后, 我们要创建一个包含新键的节点副本.

func leafInsert(new BNode, old BNode, idx uint16, key []byte, val []byte) {
	new.setHeader(BNODE_LEAF, old.nkeys()+1)
	nodeAppendRange(new, old, 0, 0, idx)
	nodeAppendKV(new, idx, 0, key, val)
	nodeAppendRange(new, old, idx+1, idx, old.nkeys()-idx)
}

nodeAppendRange函数从旧节点拷贝键至新节点

func nodeAppendRange(new BNode, old BNode, dstNew uint16, srcOld uint16, n uint16) {
	assert(srcOld+n <= old.nkeys())
	assert(dstNew+n <= new.nkeys())
	if n == 0 {
		return
	}
	for i := uint16(0); i < n; i++ {
		new.setPtr(dstNew+i, old.getPtr(srcOld+i))
	}
	dstBegin := new.getOffset(dstNew)
	srcBegin := old.getOffset(srcOld)
	for i := uint16(1); i < n; i++ {
		offset := dstBegin + old.getOffset(srcOld+i) - srcBegin
		new.setOffset(dstNew+i, offset)
	}
	begin := new.kvPos(srcOld)
	end := new.kvPos(srcOld+n)
	copy(new.data[new.kvPos(dstNew):], old.data[begin:end])
}

nodeAppendKV函数复制一个kv对到新节点中

func nodeAppendKV(new BNode, idx uint16, ptr uint64, key []byte, val []byte) {
	new.setPtr(idx, ptr)
	pos := new.kvPos(idx)
	binary.LittleEndian.PutUint16(new.data[pos+0:], uint16(len(key)))
	binary.LittleEndian.PutUint16(new.data[pos+2:], uint16(len(val)))
	copy(new.data[pos+4:], key)
	copy(new.data[pos+4+uint16(len(key)):], val)
	new.setOffset(idx+1, new.getOffset(idx)+4+uint16(len(key)+len(val)))
}

递归插入

插入key的主要功能

func treeInsert(tree *BTree, node BNode, key []byte. val []byte) BNode {
	new := BNode(data: make([]byte, 2 * BTREE_PAGE_SIZE))
	idx := nodeLookupLE(node, key)
	switch node.btype() {
	case: BNODE_LEAF:
		if bytes.Equal(key, node.getKey(idx)) {
			leafUpdate(new, node, idx, key, val)
		} else {
			leafInsert(tree, new, node, idx, key, val)
		}
	case BNODE_NODE:
		nodeInsert(tree, new, node, idx, key, val)
	default:
		panic("bad node!")
	}
	return new
}

leafUpdate函数与leafInsert函数类似

处理内部节点

现在来处理内部节点.

func nodeInsert(tree *BTree, new BNode, node BNode, idx uint16, key []byte, val []byte) {
	kptr := node.getPtr(idx)
	knode := tree.get(kptr)
	tree.del(kptr)
	knode = treeInsert(tree, knode, key, val)
	nsplit, splited := nodeSplit3(knode)
	nodeReplaceKidN(tree, new, node, idx, splited[:nsplit]...)
}

分裂大节点

在节点中插入键会使其增加大小, 导致其超过页大小. 在这种情况下, 节点被分割成多个更小的节点.
允许的最大键大小和值的大小只能保证单个kv键值对始终适合于一个页.在最坏的情况下, 胖节点会分裂成3个节点(中间有一个大的kv键值对)

func nodeSplit2(left BNode, right BNode, old BNode) {
	
}

func nodeSplit3(old BNode) (uint16, [3]BNode) {
	if old.nbytes() <= BTREE_PAGE_SIZE {
		old.data = old.data[:BTREE_PAGE_SIZE]
		return 1, [3]BNode{old}
	}
	left := BNode{make([] byte, 2 * BTREE_PAGE_SIZE)}
	right := BNode{make([]byte, BTREE_PAGE_SIZE)}
	nodeSplit2(left, right, old)
	if left.nbytes() <= BTREE_PAGE_SIZE {
		left.data = left.data[:BTREE_PAGE_SIZE]
		return 2, [3]BNode{left, right}
	}
	leftleft := BNode{make([]byte, BTREE_PAGE_SIZE)}
	middle := BNode{make([]byte, BTREE_PAGE_SIZE)}
	nodesplit2(leftleft, middle, left)
	assert(leftleft.nbytes() <= BTREE_PAGE_SIZE)
	return 3, [3]BNode{leftleft, middle, right}
}

更新内部节点

在节点中插入一个键可以产生1,2或3个节点. 父节点必须相应地更新自身. 更新内部节点的代码类似于更新叶子节点的代码.

func nodeReplaceKidN(tree *BTree, new BNode, old BNode, idx uint16, kids ...BNode) {
	inc := uint16(len(kids))
	new.setHeader(BNODE_NODE, old.nkeys()+inc-1)
	nodeAppendRange(new, old, 0, 0, idx)
	for i, node := range kids {
		nodeAppendKV(new, idx+uint16(i), tree.new(node), node.getKey(0), nil)
		nodeAppendRange(new, old, idx+inc, idx+1, old.nkeys()-(idx+1))
	}
	nodeAppendRange(new, ol, idx+inc, idx+1, old.nkeys()-(idx+1))
}

我们已经完成了B树的插入操作. 删除操作和剩下代码将在下一篇进行介绍.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/559239.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Bug可以说是一种缺陷吗?

我叫缺陷&#xff0c;从被创建至关闭&#xff0c;到最后做缺陷分析&#xff0c;这是我的完整生命周期。我的整个生命周期贯穿着整个项目的项目周期&#xff0c;因此&#xff0c;掌握我的生命周期&#xff0c;不止是测试人员必修的课程&#xff0c;也是测试人员的灵魂。 缺陷的…

Android 动态加载资源

资源文件分类 1.android资源文件分为两类&#xff1a; 第一类是res目录下存放的可编译资源文件&#xff0c;编译时&#xff0c;系统会自动在R.java中生成资源文件的十六进制值&#xff0c;如下所示&#xff1a; public final class R {public static final class id {public …

从注解@EventListener和@TransactionalEventListener掌握Spring的事件机制原理

文章目录 Spring事件监听机制Spring事件监听机制概述Spring事件监听机制介绍Spring事件相关的几个类使用硬编码简单还原Spring事件机制 Spring事件机制正确的使用方式Spring事件创建Spring事件发布方式Spring事件监听方式面向接口的方式面向注解的方式EventListenerTransaction…

SpringBoot开发实用篇2---与数据层技术有关的替换和整合

四、数据层解决方案 1.SQL 现有数据层解决方案技术选型&#xff1a;DruidMyBatis-plusMySQL 数据源&#xff1a;DruidDataSource 持久化技术&#xff1a;MyBatis-plus/MyBatis 数据库&#xff1a;MySql 内置数据源&#xff1a; SpringBoot提供了3种内嵌的数据源对象供开发者选…

2023年数据治理企业老板为啥都让员工考CDGA/CDGP证书?

企业老板让员工考取CDGA证书一般有以下几个原因: 提升数据治理能力 CDGA认证是一种全球通用的数据治理认证&#xff0c;可以帮助员工提升数据治理的技能和能力更好地管理、保护和分析企业的数据。 增强员工竞争力 随着数据治理在企业中的重要性越来越高&#xff0c;拥有CDGA…

day01_单元测试_配置文件

一、软件的生命周期 **软件的可行性分析:**分析该软件是否值的研发,会消耗多少成本,能带来多少的利益等分析 **需求分析:**分析该软件具体该具备有那些功能,产品经理与客户一起讨论 **软件设计:**该软件应该使用什么样的架构,用什么样的数据库,每个模块的具体功能 **程序编…

Github Copilot 的补强工具Github Copilot Labs的常用功能介绍

一、什么是Github Copilot Labs Github Copilot Labs是由GitHub推出的一款基于人工智能技术的代码协作工具&#xff0c;旨在协助开发者更加快速、高效地编写代码。该工具使用了机器学习技术&#xff0c;通过学习大量的开源代码和编写实践&#xff0c;提供了对于代码变量、函数…

物理删除与逻辑删除

目录 一、物理删除与逻辑删除 二、逻辑删除实现 三、API使用方法 四、全局配置参数 一、物理删除与逻辑删除 物理删除&#xff1a;指文件存储所用到的磁存储区域被真正的擦除或清零&#xff0c;这样删除的文件是不可以恢复的&#xff0c;物理删除是计算机处理数据时的一个概…

怎样的年轻化法则,让这个品牌四年净利润复合增速达30%

年轻世代消费者的崛起&#xff0c;从消费层面讲&#xff0c;为市场带来活跃的同时&#xff0c;给品牌带来的是如何转型升级的问题&#xff0c;在众多转型的品牌中&#xff0c;年轻化策略与方式不尽相同。 在2019年至2022年期间&#xff0c;报喜鸟营收复合增速达10%&#xff0c…

iptables防火墙(2)

iptables防火墙&#xff08;2&#xff09; 一、SNATSNAT应用环境SNAT原理SNAT转换前条件扩展 二、DNATDNAT应用环境DNAT原理DNAT转换前提条件扩展 三、防火墙规则的备份和还原导出&#xff08;备份&#xff09;所有表的规则导入&#xff08;还原&#xff09;规则 一、SNAT SNA…

线性回归和预测

目录 1、线性回归 2、R-Squared 1、线性回归 在机器学习和统计建模中&#xff0c;这种关系用于预测未来事件的结果 线性回归使用数据点之间的关系在所有数据点之间画一条直线 这条线可以用来预测未来的值 在机器学习中&#xff0c;预测未来非常重要。比如房价、股票等预测 …

分布式全局唯一id实现-4 springCloud-MyBatis-Plus集成美团分布式全局id(leaf)

前言&#xff1a;美团的leaf集成了db分段生成id和雪花算法生成分布式id&#xff0c;本文对其实现部分细节展开讨论&#xff0c;leaf 的具体实现请参考&#xff1a;https://tech.meituan.com/MT_Leaf.html&#xff1b; 1 使用db分段id&#xff1a; leaf 的分段id本质上是使用了…

5。STM32裸机开发(5)

嵌入式软件开发学习过程记录&#xff0c;本部分结合本人的学习经验撰写&#xff0c;系统描述各类基础例程的程序撰写逻辑。构建裸机开发的思维&#xff0c;为RTOS做铺垫&#xff08;本部分基于库函数版实现&#xff09;&#xff0c;如有不足之处&#xff0c;敬请批评指正。 &…

二本4年测试经验,3面阿里艰苦经历(定薪25K),上岸那天我哭了...

前言 4月准备跳槽&#xff0c;先后面试了各大小公司&#xff0c;拿了一些小offer&#xff0c;面试的公司大部分都能过&#xff0c;但是做人总是要用梦想吧&#xff0c;没有梦想和咸鱼有什么区别&#xff0c;最终把目标放在了阿里&#xff0c;准备了大概3个月的时间&#xff0c…

mysql45讲笔记

不一定要都学&#xff0c;有些感觉用不到&#xff0c;有选择的学&#xff01;&#xff01;&#xff01; 文章目录 mysql45讲1.mysql基础架构2.mysql日志系统3.事务隔离4.索引类型1.哈希表2.有序数组3.二叉搜索树4.B 树 5.索引重点概念覆盖索引索引下推最左前缀原则 6.全局锁表级…

ERP系统是什么?ERP实施顾问怎么做?

ERP实施顾问怎么做&#xff1f; 首先想要从事相关行业&#xff0c;必须先了解什么是ERP&#xff0c;ERP系统功能模块是怎样的&#xff0c;而后才能进行ERP实施顾问的工作。 一、ERP是什么 ERP系统主要是干什么的&#xff1f;ERP系统&#xff0c;简单理解就是一套记账、做账软…

“全球金融科技大会——中国金融业开源技术应用与发展论坛”在北京举行

3月28日&#xff0c;“全球金融科技大会——中国金融业开源技术应用与发展论坛”在北京新动力金融科技中心举行。 会议现场 人民银行科技司二级巡视员杨富玉&#xff0c;开放原子开源基金会理事长孙文龙&#xff0c;中国金电党委书记、董事长周逢民为大会致辞。北京市西城区区…

(转载)MATLAB智能算法30个案例分析(3)——基于遗传算法的BP神经网络优化算法

1 理论基础 1.1 BP神经网络概述 BP网络是一类多层的前馈神经网络。它的名字源于在网络训练的过程中&#xff0c;调整网络的权值的算法是误差的反向传播的学习算法&#xff0c;即为BP学习算法。BP算法是Rumelhart等人在1986年提出来的。由于它的结构简单&#xff0c;可调整的…

docker+redis哨兵模式(一主二从三哨兵)- docker-compose

一、docker-compose 安装&#xff1a; sudo apt-get update #安装最新的docke-ce sudo apt-get install docker-ce # 下载最新的docker-compose curl -L https://github.com/docker/compose/releases/download/1.25.0-rc4/docker-compose-uname -s-uname -m -o /usr/local…

面试字节,过关斩将直接干到 3 面,结果被吊打了?

人人都有大厂梦&#xff0c;对于软件测试员来说&#xff0c;BAT 为首的一线互联网公司肯定是自己的心仪对象&#xff0c;毕竟能到这些大厂工作&#xff0c;不仅薪资高待遇好&#xff0c;而且能力技术都能够得到提升&#xff0c;最关键的是还能够给自己镀上一层金&#xff0c;让…