B树磁盘页管理的核心是解耦内存节点生命周期与页号,必须用PageID(uint64)而非*Node指针,因指针序列化后失效、重启崩溃且引发GC和数据错乱;页大小默认4096以对齐OS页缓存与磁盘I/O。

直接说结论:B树磁盘页管理的核心不是“怎么读写文件”,而是“如何把内存节点生命周期和页号(page ID)解耦”。用*Node当指针会崩溃,用uint64页号+回调函数才是正解。
为什么不能用*Node指针存磁盘页
Go 的 *Node 是内存地址,序列化到磁盘后毫无意义;重启进程后所有指针失效,tree.root 变成悬空指针,panic: invalid memory address 是必然结果。更隐蔽的问题是 GC —— 如果你把 *Node 存进 map 或 channel,又没及时清理,GC 无法回收对应内存,但磁盘页其实早已被覆写,数据错乱静默发生。
常见错误现象:
- 程序运行时正常,重启后查不到刚写入的 key
- 插入几万条后,
tree.Search()返回nil却不报错 - 用
unsafe.Pointer强转页号再还原,结果在不同架构(arm64 vs amd64)上字节序错乱
正确做法是把页号当作“逻辑句柄”:
立即学习“go语言免费学习笔记(深入)”;
- 定义
type PageID uint64,所有节点间引用只传PageID - 提供
get(PageID) BNode和put(BNode) PageID两个回调,由上层决定从内存缓存取、还是从 mmap 文件读、或是走 WAL 日志回放 - 根节点不存
*Node,而存root PageID,初始化时调用get(root)加载
BTREE_PAGE_SIZE 设为 4096 还是 8192?
页大小不是越大越好,也不是越小越灵活,它直接绑定操作系统页缓存行为和磁盘 I/O 效率。4096 是绝大多数 Linux 默认 page size,也是 ext4/xfs 文件系统最小分配单元。设为 4096 能让 mmap 映射、read() 系统调用、以及 SSD 的 NAND 页擦写天然对齐。
设为 8192 的代价:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 单次
read()多读 4KB 冗余数据,尤其在范围查询跳页时放大无效 IO - 内存碎片加剧:一个 4096 页能塞下约 128 个
int64key,而 8192 页若只填了 70 个 key,浪费率超 45% - boltdb、badger 等成熟引擎全默认 4096,不是巧合——它是硬件与 OS 协同优化的交点
例外场景只有两种:BTREE_MAX_KEY_SIZE > 1000(需更大空间塞单条大 key),或明确跑在 64KB 页对齐的 ZFS 上。否则别碰 8192。
节点分裂时,如何安全更新父页的子页指针
分裂不是“复制一半 key + 新建一页”就完事。关键在于父子页指针的原子更新顺序。如果先写新叶子页,再改父页,中间 crash,就会出现“父页指向一个不存在的页号”,下次加载直接 panic。
必须按严格顺序操作:
- 先调用
new(leafNode)获取新PageID,但**不写入任何数据** - 把新叶子的
keys和next填好,调用put()持久化——此时新页已落盘可读 - 再读取父页(
get(parentID)),在其keys切片中插入新 key(即新叶子的最小 key),在children切片对应位置填入新PageID - 最后
put(parent)覆盖写父页——这一步失败,最多多出一个孤立新叶子页,可被后台 GC 清理
注意:children 切片长度必须恒等于 len(keys) + 1(B+ 树内部节点特性),少一个索引就偏移,多一个则后续 sort.Search 定位失败。
删除后合并叶子页,next 链表怎么不断
很多人只处理了 key 删除和子页指针收缩,却忘了 next 是链表核心。A → B → C,若 B 被合并进 A,则 A.next 必须从 B 改为 C,否则 Range(100, 200) 扫到 A 就停,永远查不到 C 中的数据。
合并逻辑必须三步闭环:
- 拿到待合并页 B 的
next PageID(记为nextID) - 将 B 的所有
keys和values追加进 A 的末尾,并保持有序 - 调用
get(nextID)加载 C,设置A.next = nextID,再put(A);同时设置C.prev = A.PageID(如果实现双向链表),再put(C)
漏掉最后一步,next 更新只发生在内存,磁盘页没同步,重启后链表照样断裂。这也是为什么 boltdb 的 freelist 不光管空闲页,还要维护页间拓扑关系。

















