CMU_15_445_project_2_B+Tree
CMU_15_445_project_1_buffer_pool
Overview
实现一个 B+ 树,需要支持线程安全、搜索、插入、删除(包括拆分和合并节点)以及迭代器以支持按顺序扫描叶子
CHECKPOINT #1
Task #1 - B+Tree Pages
这个任务需要实现3哥B+Tree的类:
B+Tree Page
一个基类,Internal Page 和Leaf Page 都从它继承,它只包含两种子类共有的信息,page_type_ ,size_ ,max_size_
只要实现这几个变量的get set方法就行了,没什么好说的
B+Tree Internal Page
Internal Page 存储了m个有序的key和m+1个指向其他B+树页面的指针(以page_id表示).这些key和指针是以key/指针对的形式存储在一个数组中.由于指针的数量不等于键的数量,所以第一个键是无效的,查找时应该从第二个键开始.
B+树内部页面应该保持至少半满的状态.在删除时,两个半满的页面可以合并,或者键和指针可以重新分配来避免合并.在插入时,一个满的页面可以分裂成两个,或者键和指针可以重新分配来避免分裂.这些都是实现B+树时需要做出的设计选择.
第一个key为nullptr,他的指针指向第一个子节点
关于 MappingType array_[0]
BPlusTreeInternalPage 这个类有个 MappingType array_[0] 变量,是一个flexible array,当我们初始化一个数组时并不知道这个数组有多大,但是我们知道分配给这个类对象多大的空间,flexible array 会自动填充,占用未被其他变量使用的内存.这样就可以确定自己的长度了.
B+Tree Leaf Page
B+Tree Leaf Page 存储了m个有序的键和它们对应的m个值.值应该只是64位的记录ID,用于定位实际元组存储的位置,叶子页面和内部页面一样,对键/值对的数量有限制,而且应该遵循相同的操作来合并、分裂和重新分配键.
虽然Leaf Page和Internal Page包含相同类型的键,但它们可能有不同类型的值.因此,max_size可以不同.
每个B+树Leaf/Internal Page对应于缓冲池获取的内存页面的内容(即data_部分).每次读或写一个叶子或内部页面,你必须先从缓冲池中获取页面(使用它的唯一page_id),将其重新解释为叶子或内部页面,并在读或写后取消固定页面.
Task #2a - B+Tree Insertion and Search for Single Values
实现单个值的插入Insert()和查找GetValue()操作.
GetValue
按照原教材的伪代码实现,使用二分查找 lower_bound,依次遍历直到找到叶子节点,把遍历到的节点一次追加到上下文管理的read_set_里,这样离开作用域的时候会自动释放页面的读锁.
Context ctx;
page_id_t cur_page_id = GetRootPageId();
ctx.read_set_.push_back(bpm_->FetchPageRead(cur_page_id));
Insert
插入的操作会比查找难一点,教材也给出了详细的伪代码
首先需要从根页面开始,按照B+树的插入规则逐级向下查找合适的叶子节点.如果插入的键已经存在于树中,则不执行插入操作,并返回false.否则,根据B+树的规则将键值对插入到叶子节点中.如果插入导致叶子节点超出容量限制,则进行节点的分裂操作,并把中间key传给父节点,如果父节点也需要分裂则一直递归到根节点,直到不分裂为止,如果分裂操作导致根节点变化,需要更新根页面的ID.
tips 1
BPlusTree 类通过PageId拿到page的指针然后强转得到,在类里修改变量会同步修改page的data_,如图在初始化B+树时把root_page_id修改为2341234,page的data_的内存地址0x621000001500数据会变为2341234.
tips 2
在 BPlusTree 类里实现 InsertInParent 函数的时候需要先把原来节点的 array_ 数据新建一个临时空间拷贝过去,不然在原地址插入会污染后面的内存空间,然后再从临时空间分段拷贝到原节点和新建节点,注意 internal 里面的数据是 std::pair<KeyType, page_id_t>
格式的,这里能用 MappingType std::pair<KeyType, ValueType>
宏来判断内存大小.
tips 3
代码中给出的 header_page_id_ 和 root_page_id 是不同的,header相当于一个索引页面,是root节点的父节点,header页面只保存一个变量那就是root_page_id,每次调用GetRootId方法会用 header_page_id_ 去缓冲池里拿到 header 页面,然后强转成 BPlusTreeHeaderPage 类再拿到root_page_id.
tips 4
申请 NewPage 用 NewPageGuarded 包装类,不然申请的时候 pin_count++ 了这个页面没有释放之后 BPM 满了就申请不到了.
在调用 BPM FetchPage 之后一定记得要 UnpinPage,不然后面申请不到 NewPage.
CHECKPOINT #2
Task #2b - B+Tree Deletions
实现B+树的 Delete,包括在必要时合并或重新分配节点中的键,以保持B+树的不变性.与插入一样,如果根节点发生变化,必须正确地更新B+树的root_page_id.
这篇大佬写的博文有图文明细可参考 CMU 15445-2022 P2 B+Tree Insert/Delete
按照教材的伪代码一步步实现,每一次 Insert 和 Remove 之后用官方给出的 Draw 画出整棵树 Debug.
很难,非常难,debug 前前后后花了两周的时间才把 remove 方法实现,坑很多,很多细节需要注意,慢慢打日志 debug 吧.
注意删除节点之后 next_page_id也需要重新设置!!!
Task #3 - An Iterator for Leaf Scans
添加一个C++迭代器,高效地支持叶节点的顺序扫描.存储当前页面的 ReadGuard,LeafPage* 和 index,当 index 为 -1 时则为 end,++的操作需要判断是否是当前 page 的 last index,如是则跳到下一个 page first index.
Task #4 - Concurrent Index
把代码中所有用到 newpage() fetchPage() 函数的地方替换成 guard 操作.一是避免忘记 unpinge 造成的缓冲池没有空闲队列,二是持有读写锁避免并发问题.
在 Insert 构建 Context 的时候,当确保节点不会发生分裂,则清空 ctx 的 guard 队列,避免过长时间的占用. 父节点递归的时候也要把没用的子节点 pop.
在 Remove 构建 Context 的时候,当确保节点不会发生 borrow or merge,则删除 ctx 的 guard 队列除了最后一个元素的所有元素(持有父节点的锁),避免释放父节点锁导致另外线程通过父节点拿到另外的叶子节点,然后另外的叶子节点删除操作的时候造成了 merge,而当前叶子节点还未完成删除操作.
submit
checkpoint2 的 MixTest2 说我内存泄漏,是在 Insert 函数内调用 NewPage 函数发生的,死活找不到原因
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」