B+Tree索引
插入与删除
主要需要注意分裂与合并。分裂时,叶子节点是先分裂再插入数据,而内部节点需要先插入再分裂。
删除时,先删除,如果size小于minsize则需要向左右兄弟节点借数据,如果还不行则合并。
有一个特殊情况就是没有兄弟节点,尽管这在真实索引使用中是不可能的,但是在在线测试中,有些测试中internal_max_size = 3,这就会发生没有兄弟节点的情况。对于这种情况,笔者的处理是只删除数据不删除节点,但是在使用迭代器时,需要对operator++进行特殊处理。
迭代器
笔者的数据成员如下
BufferPoolManager *bpm_{nullptr};
page_id_t leaf_page_id_{INVALID_PAGE_ID};
int index_{0};
而在operator++中需要跳过一些空节点,核心代码如下
while (leaf_page_id_ != INVALID_PAGE_ID && index_ >= leaf_page_ptr_->GetSize()) {
index_ = 0;
leaf_page_id_ = leaf_page_ptr_->GetNextPageId();
read_guard_page = bpm_->FetchPageRead(leaf_page_id_);
leaf_page_ptr_ = read_guard_page.As<BPlusTreeLeafPage<KeyType, ValueType, KeyComparator>>();
}
并发
笔者的并发策略基于这样的一个判断:发生分裂/合并的插入/删除操作占比较小。因此,对于插入操作,第一次插入,只获取叶子节点的写锁,如果不需要分裂,则插入后返回,否则进行第二次插入。
第二次插入,获取根节点到页子节点的写锁,从叶子往上,进行递归插入。
删除操作同理。
这样做的好处是,对于第一次插入,不会阻塞读操作,同时也能支持对不同叶子节点的插入操作。
debug
对于本地测试,可以使用vscode debug。比较难的是并发的debug,这里说下笔者印象较深的bug,就是是FetchWritePage中,bpm锁、pin_count和page写锁的管理,最后通过的代码是:先获取bpm锁,然后pin_count++,然后释放bpm锁,再申请page写锁。
除了互斥资源,锁的释放如果被阻塞了,也可能引发死锁。
对于在线测试,笔者只会用经典方式:cout,打印一些信息来推测大概哪里出问题。
测试与总结
到提交为止(2023-05-13),gradescope上有42人完成了project2。由于缓冲区管理用的是全局锁,笔者的B+树并发性能算是一般。
笔者之前也写过B+Tree,但是是单线程内存B+Tree,这次多线程磁盘B+Tree还是挺困难的。
相较于内存索引,磁盘索引还需要管理缓存区。
另外两者的I/O粒度也不同,前者是cpu cacheline,后者是磁盘页/块。这导致两者在设计的着重点也不同,内存B+Tree的节点较小,需要充分利用每一个字节。
总之,这次实验增加了对面向磁盘B+Tree的理解,积累了并发编程以及debug的经验。