CMU15-445 Project #2 - B_PLUS_TREE Checkpoint 1 and 2

CMU15-445 Project #2 - B_PLUS_TREE Checkpoint 1 and 2

前言

写完Project1摸了好几天顺便过了个假期,回来赶紧把Project 2写了,Project 2其实分两部分:

  • 第一部分是实现单线程的B_PlUS_TREE的增删查,其中checkpoint1只要实现增查,删放在了checkpoint2中,
  • 第二部分是实现在单线程的基础上实现并发的B_PLUS_TREE,放在了checkpoint2中,这也是这个Project的难点,但是因为搞过6.824,感觉这个的并发BUG不是很难找,随便(大半天)打打LOG就出来了。
    由于这个实验记录是写完Checkpoint2写的,所以可能会弄混Checkpoint1、2各自实现了什么,还请谅解。

Checkpoint1

这一部分主要是实现leaf_pageinternal_page以及tree_page的各种辅助函数以及BPlusTree类的实际执行的insertgetvalue。其中leaf_pageinternal_page可能有一些接口看起来比较迷惑,刚开始写的时候也不知道这接口有啥用,就按照接口的名字结合网上的博客所描述的接口含义写了,实际上在写insertgetvalue以及delete的时候才知道这些接口有什么用,于是删了一大半重写,所以这里推荐先看一遍这些接口的接口说明注释,不去具体实现,在写增删查的时候自然就能根据名字对应上了。具体的page函数可以看下面的函数说明理解一下。

insert && getvalue

insertCheckpint1的难点,但是由于不涉及并发,所以只要按照书中(《Database-System-Concepts-7th-Edition》)所描述的伪代码实现即可。理解了之后没有特别大的难度,无论是insert还是getvalue首先要实现的就是FindLeafPage,FindLeafPage其实就是调用page中的接口去找到对应的孩子。

MinSize和MaxSize问题

由于internal的第一个K是无效的,所以第一次看会让人会迷惑,实际上我们按书上的定义理解即可,由于就算第一个K是无效的,也占一个index所以我使用的策略是,对于internal_page,如果达到MaxSize不进行分裂,超过才分裂,而对于leaf_page达到MaxSize才分裂,而对于MinSize,都是小于MinSize触发合并或者向邻居借用KV

Checkpoint2

这一部分其实分两个小部分,如下:

Delete

第一个小部分是把没有实现的delete(remove)给实现了,deleteinsert要难,并且代码中的函数接口和书中的还有些许的差别,但是逻辑上都是一样的,需要注意的就是CoalesceOrRedistributeCoalesce的返回值,前者是参数中的node是否需要删除,后者是参数中的parentnode是否需要删除,合理利用即可正确地删除page

并发控制

这一部分是整个project的难点,由于写的比较晚,已经有很多小伙伴在pizza上写了踩坑记录了,所以幸得避开了很多坑,但是还是写了整整1天才调出来满分通过。
并发控制中最难得是对于root_page_id_的保护,看了pizza上了讨论贴,知道了很多不同的方案,最后选了我觉得好实现的写。

有个坑是在delet的时候需要对sibling加锁,因为有可能还有线程在对sibling进行操作例如slibing由线程1进行操作,由于slibing很安全,所以以上的锁都释放了,线程2如果不获取slibing的锁直接进行操作,就会导致错误。

root_page_id_加锁方案

这里再次感谢一下pizza的小伙伴对于加锁方案的讨论,根据讨论主要分为两种:

  • 第一种是增加一个虚拟节点,无论是使用header_page还是增加一个虚拟节点都属于此类,这种我没实现过,具体细节不太清楚
  • 第二种是增加一个对root_page_id_std::mutex的锁,这个好像也是Lab的hint中所推荐使用的方式,我使用的是这种方法,并且采用的是讨论区中Bing Zijie所述的使用nullptr扔进transaction中标记对于该root_page_id_使用了std::mutex,以方便解锁。而对于其他的扔进transactionpage*,则正常解锁R或者W锁。

加锁具体实现

具体实现就是使用transaction维护deletesetunlock&unpin set,在findleafpage的时候把page*往里面扔,safe或者操作结束的时候释放锁unpin相对page或者删除page等。这里要注意的是对于transaction=nullptr的情况要进行特判,不要忘记了解锁以及unpin page,不然会导致bpm有一堆没有被unpinpage,可以写一个PrintBPM,看看每次操作后是不是所有pagepincount都等于零。

螃蟹规则

螃蟹规则就是从root出发一路向下找的同时,对路过的page都加了R或者W锁,如果确认安全的情况下,可以释放之前的锁,安全的情况是指比如现在有个指向当前leaf or internal pagenowpageptr以及要找到的下一个leaf or internal pagechildpageptr,如果执行的是delete,满足childpageptr.Size()-1>=childpageptr.GetMin(),也就是就算删了之后一路调整到childpageptr,在childpageptr进行了删除后,也不会导致后续向上的调整,所以nowpageptr及其以上的所以锁都可以释放了。其他操作也是同理的,当然,read操作每次都是safe的,可以直接释放。

这里andy还讲了乐观悲观锁的优化,也就是无论是read还是delete,insert,都先加R锁,如果没影响可以开心地操作,有影响再来一遍加W锁的操作,对于读多写少的场景很有帮助。

iterator 实现

需要实现iterator
b_plus_tree.cpp中需要实现如下:

INDEXITERATOR_TYPE BPLUSTREE_TYPE::begin()
返回最左边叶子iterator
INDEXITERATOR_TYPE BPLUSTREE_TYPE::Begin(const KeyType &key)
返回包含key叶子的iterator
INDEXITERATOR_TYPE BPLUSTREE_TYPE::end()
结尾iterator

index_iterator.cpp中需要实现如下

INDEXITERATOR_TYPE::IndexIterator(LeafPage *Leafptr, int32_t index, Page *pageptr,BufferPoolManager *buffer_pool_manager)
新增加的构造函数,方便刚刚的begin之类的使用

INDEXITERATOR_TYPE::~IndexIterator()
析构函数,注意判断pageptr==nullptr的情况以及解R锁

bool INDEXITERATOR_TYPE::isEnd()
const MappingType &INDEXITERATOR_TYPE::operator*()
顾名思义

INDEXITERATOR_TYPE &INDEXITERATOR_TYPE::operator++() 
这个是最为重要的函数,也就是类似移动cursor,当当前leaf还有值可以遍历的时候增加index,要移动到下一个叶子的时候需要unpin当前page以及fetch新page并且对当前Page解R锁以及对下一个Page加R锁


internal_page函数说明

要注意的是有的操作需要修改ParentID之类的。

KeyType B_PLUS_TREE_INTERNAL_PAGE_TYPE::KeyAt(int index) const 
返回index处的Key

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::SetKeyAt(int index, const KeyType &key)
将index处的key设置成传进来的参数key

int B_PLUS_TREE_INTERNAL_PAGE_TYPE::ValueIndex(const ValueType &value)const
根据value查找对应index,由于value是无序的,显然这里只能for一遍

ValueType B_PLUS_TREE_INTERNAL_PAGE_TYPE::ValueAt(int index) const 
返回index处的Value

ValueType B_PLUS_TREE_INTERNAL_PAGE_TYPE::Lookup(const KeyType &key, const KeyComparator &comparator) const
找到key对应的value,由于key有序,这里推荐二分

int B_PLUS_TREE_INTERNAL_PAGE_TYPE::KeyIndex(const KeyType &key, const KeyComparator &comparator) const
找到key对应的index,我这里实现的是找到第一个大于等于key的index,如果一个都没有,那么会返回当前internal_page中最大index的下一个index,其他细节交由调用者维护。

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::PopulateNewRoot(const ValueType &old_value, const KeyType &new_key,
                                                     const ValueType &new_value)
insert后一直分裂到了root,这里是为新root设置2个指针value以及一个key

int B_PLUS_TREE_INTERNAL_PAGE_TYPE::InsertNodeAfter(const ValueType &old_value, const KeyType &new_key,
                                                    const ValueType &new_value) 
在old_value后面插入new_key,new_value,insert分裂的时在父节点加入新分裂节点的时候可以用到

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::MoveHalfTo(BPlusTreeInternalPage *recipient,
                                                BufferPoolManager *buffer_pool_manager)
将当前节点的一半move 到新节点recipient处,在分裂的时候会用到

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::CopyNFrom(MappingType *items, int size, BufferPoolManager *buffer_pool_manager) 
从items中复制size个KV放到自己的array最后,这里注意要更改这些被移动的节点的parentPageID

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::Remove(int index) 
删除index处的节点

ValueType B_PLUS_TREE_INTERNAL_PAGE_TYPE::RemoveAndReturnOnlyChild() 
删除节点并且返回唯一的孩子,当root只有一个孩子的时候,只需要让它的孩子成为新root,删除原root,从而减少树高

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::MoveAllTo(BPlusTreeInternalPage *recipient, const KeyType &middle_key,
                                               BufferPoolManager *buffer_pool_manager)

把array全部复制到recipient中,然后删除,在delelte的合并的时候会用上

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::MoveFirstToEndOf(BPlusTreeInternalPage *recipient, const KeyType &middle_key,
                                                      BufferPoolManager *buffer_pool_manager) 
把自己的第一个节点删除并且复制到recipient中,删除的时候向邻居节点借儿子的时候会使用

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::CopyLastFrom(const MappingType &pair, BufferPoolManager *buffer_pool_manager)
在结尾append一个KV

void B_PLUS_TREE_INTERNAL_PAGE_TYPE::MoveLastToFrontOf(BPlusTreeInternalPage *recipient, const KeyType &middle_key,
                                                       BufferPoolManager *buffer_pool_manager) 
把最后一个KV移动到recipient的开头


void B_PLUS_TREE_INTERNAL_PAGE_TYPE::CopyFirstFrom(const MappingType &pair, BufferPoolManager *buffer_pool_manager)
把pair放到开头

leaf_page函数说明

leaf_page的实现大致与internal_page相同,这里只记录有较大差别的

page_id_t B_PLUS_TREE_LEAF_PAGE_TYPE::GetNextPageId()
顾名思义,下一个leaf的ID

void B_PLUS_TREE_LEAF_PAGE_TYPE::SetNextPageId
设置下一个leaf的ID

int B_PLUS_TREE_LEAF_PAGE_TYPE::Insert(const KeyType &key, const ValueType &value, const KeyComparator &comparator)
插入一对KV 

bool B_PLUS_TREE_LEAF_PAGE_TYPE::Lookup(const KeyType &key, ValueType *value, const KeyComparator &comparator) const 
该leaf是否存在key,是返回true,这里注意要判断value是不是nullptr

int B_PLUS_TREE_LEAF_PAGE_TYPE::RemoveAndDeleteRecord(const KeyType &key, const KeyComparator &comparator)
删除的key,返回Size(),存在该key就删除,不存在就立即返回

void B_PLUS_TREE_LEAF_PAGE_TYPE::MoveAllTo(BPlusTreeLeafPage *recipient, BufferPoolManager *buffer_pool_manager) 
这里实现了将所以值移动到recipent后面,delete合并的时候需要用到

leaf参数更改说明

以上leaf_page 和internal_page部分函数参数做出了更改,应该在b_plus_tree.cpp中使用了typename N ,让参数一致方便调用。

结果


这个时间会根据实现在一定范围内波动的

posted @ 2021-10-08 22:26  tttttttttrx  阅读(665)  评论(0编辑  收藏  举报