CMU_15_445_project_2_B+Tree

发布时间 2023-07-26 00:49:28作者: autumn814

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));

img

Insert

插入的操作会比查找难一点,教材也给出了详细的伪代码

首先需要从根页面开始,按照B+树的插入规则逐级向下查找合适的叶子节点.如果插入的键已经存在于树中,则不执行插入操作,并返回false.否则,根据B+树的规则将键值对插入到叶子节点中.如果插入导致叶子节点超出容量限制,则进行节点的分裂操作,并把中间key传给父节点,如果父节点也需要分裂则一直递归到根节点,直到不分裂为止,如果分裂操作导致根节点变化,需要更新根页面的ID.

img

img

tips 1

BPlusTree 类通过PageId拿到page的指针然后强转得到,在类里修改变量会同步修改page的data_,如图在初始化B+树时把root_page_id修改为2341234,page的data_的内存地址0x621000001500数据会变为2341234.

img

img

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 吧.

img

注意删除节点之后 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

img

checkpoint2 的 MixTest2 说我内存泄漏,是在 Insert 函数内调用 NewPage 函数发生的,死活找不到原因

img

img