CMU15-445 Project.2总结

在线测试
在这里插入图片描述

在这里插入图片描述

Project #2 - B+Tree

以下是Project #2的网址,2022FALL的Project #2是实现一个B+树动态索引结构,用于实现数据库内的数据检索。我们可以根据Project 1 中实现的 buffer pool manager 来获取 page ,在此基础上实现四个 Task :

  1. 实现B+树的设计,其中树页面是要分为三种:BPlusTreePage 、BPlusTreeInternalPage 、BPlusTreeLeafPage ,分别表示B+树种的基础节点、基础节点上的内部节点和基础节点上的叶节点;
  2. 实现B+树的数据结构,即实现B+树的插入、删除、查询;
  3. 实现B+树种叶节点的索引迭代器,即实现 iterator 的多项功能;
  4. 实现B+树的并发操作。

1、任务 1 - B+树页面

B+树的页面是要分为三种:BPlusTreePage 、BPlusTreeInternalPage 、BPlusTreeLeafPage 。对于一个 page 而言,包括了:

/** The actual data that is stored within a page. */
char data_[BUSTUB_PAGE_SIZE]{};
/** The ID of this page. */
page_id_t page_id_ = INVALID_PAGE_ID;
/** The pin count of this page. */
int pin_count_ = 0;
/** True if the page is dirty, i.e. it is different from its corresponding page on disk. */
bool is_dirty_ = false;
/** Page latch. */
ReaderWriterLatch rwlatch_;

其中,data_ 是实际存放 page 数据的地方,其余的属性成员存放在剩余地区。BPlusTreePage 中包括了:

  IndexPageType page_type_;
  lsn_t lsn_;
  int size_;
  int max_size_;
  page_id_t parent_page_id_;
  page_id_t page_id_;

这些数据存放在 data_ 的开头,及作为 header ,剩下的地区继续用于存放 tree page 的数据。BPlusTreeInternalPage 中包括了 MappingType array_[1];;BPlusTreeLeafPage 中包括了 page_id_t next_page_id_; MappingType array_[1];。其中,MappingType array_[1];是一个键值对的 flexible array ,即柔性数组,具有以下特点:flexible array 必须是类中的最后一个成员,并且仅能有一个。在为对象分配内存时,flexible array 会自动填充,占用未被其他变量使用的内存。这样做的好处是我们可以动态调整 array_ 的大小,而这实际上对应了B+树种的每一个树节点,树节点种的数据可能增加或删除。同时,在B+树中我们需要同时存储 key 和 value :1、当节点为内部节点时,key 储存其每个子节点中的最大值,即当前节点中的关键值,而 value 储存指向每个子节点页面的 page_id ;2、当节点为叶子节点时,key 储存当前节点中的关键值,而 value 储存对应关键值的具体存储值。其中,B+树要求叶子节点之间有指针指向下一个叶子节点,故使用 page_id_t next_page_id_;存储。

对于每个页面,都需要填充相应的 set 和 get 函数。其中值得注意的是,对于内部节点和叶子节点,我们还需要额外考虑其最小 size ,当一个节点的 size 小于其 MinSize 时,便需要从兄弟节点中借键值对或者和兄弟节点合成新的节点,这样的重分配或合并操作是会递归进行的,因此需要进行判断。

总结

  1. 对于B+树而言,我们用INVALID_PAGE_ID作为其根节点的父节点;
  2. 叶子节点的 MinSize 为 max_size_ / 2 ,内部节点的 MinSize 为 (max_size_ + 1) / 2
  3. 对于B+树中的叶子节点和内部节点而言,必须满足 MinSize 的要求,但对于B+树的根节点而言可以不用满足以上要求。当根节点的子节点只有一个时,我们可以直接将子节点作为新的根节点。
  4. 对于B+树种的内部节点而言,由于 K 个关键值将整个节点分成了 K+1 个区间,故其有 K+1 个 value ,而叶子节点中 key 与 value 的个数则相同。

2、任务 2 - B+树数据结构

为了实现B+树的数据结构,我们重点需要实现B+树的查询、插入、删除。我们根据不同功能进行讨论。

查询

为了实现B+树的查询,我们需要能够从根节点开始,利用 key 一直查询子节点直至查询到叶子节点,我们可以将这个功能简化为函数FindLeaf(const KeyType &key, Operation operation, Transaction *transaction, bool leftMost, bool rightMost)。其中,考虑到我们后续在实现并发控制时的上锁问题,我们需要根据我们是进行查询还是插入、删除上不同的锁,因此需要声明操作的类型。此外,考虑到我们还需要实现迭代器的一些列操作,我们可能会需要叶子节点中的 begin 或 end ,因此也需要判断我们是查找最左、最右节点还是按照 key 进行查找。在查找过程中,我们每次都查找到当前节点中 key 对应的 value 值,即对应子节点的 page_id ,而后我们更新当前的页面冲重复查询下一个节点直至当前节点为叶子节点为止。当我们获得了 key 对应的 value 时,我们利用UnpinPage声明对于页面的使用结束,并且不需要重写。

插入

为了实现B+树的插入,我们需要进行如下的流程:

  1. 若当前树为空,我们首先调用函数StartNewTree建立一个新的根节点;
  2. 若当前树不为空,我们首先利用FindLeaf,根据 key 找到需要插入的 leaf page。而后调用叶子节点中的Insert函数插入相应的键值对。
  3. 插入键值对后,判断特殊情况:1、若插入重复,我们接触线程占用,不需要重写;2、若插入后叶子节点未满,我们解除线程占用,需要重写;3、若插入后叶子节点已满我们需要调用函数Split对当前节点进行分割。
  4. 在对当前节点进行分割时,我们需要执行如下操作:1、申请新页面;2、将原 page 中后一半数组移动到新页面中;3、更新分裂后两个节点的 next page id ;4、调用函数InsertIntoParent,向父节点中插入区分两个节点的 key ,值得注意的是,我们在此时需要判断父节点是否会变满,若父节点变满我们同样需要调用Split函数对父节点进行分割,并调用InsertIntoParent对再上一层的父节点进行添加和判断,直至当前节点不再分裂或者抵达根节点为止;5、更新所有子节点的父节点指针。

在对内部节点进行分裂时,我们执行如下流程:

  1. 在插入前判断当前的 size 是否等于 max size ,若小于则肯定插入成功;
  2. 否则我们分裂当前的内部节点,我们首先申请一个新的 page ,而后将原 page 的后半部分插入新的节点中,并更新子节点的父节点指针和新节点的父指针;
  3. 继续调用函数,将区分旧内部节点和新内部节点的键值插入当前节点的父结点中并对父节点执行相同的判断。

删除

为了实现B+树的删除,我们需要进行如下的流程:

  1. 我们首先利用FindLeaf,根据 key 找到需要插入的 leaf page。而后调用叶子节点中的RemoveAndDeleteRecord函数删除相应的键值对。
  2. 在进行删除之后,我们对不同情况进行讨论:1、若删除后 size 不变,说明找不到对应的键,删除失败;2、若删除后节点的 size 大于等于 minsize ,说明删除成功,我们将要删除的页面加入最终的删除页面集合中,最终统一删除,并解除线程占用;
  3. 若节点的 size 小于 minsize ,说明我们需要向该节点的兄弟节点借键值对或和兄弟节点合并成新节点。
  4. 若当前节点不为当前兄弟节点集合中的最左侧节点,我们对其左侧节点进行判断:1、若左侧节点的 size 大于 minsize ,我们将左侧节点的最右侧键值对加入右侧节点中,并更新当前节点的在父节点中的键;2、若左侧节点的 size 小于等于 minsize ,我们将左侧节点和右侧节点进行合并,将右侧节点中的所有键值对加入左侧节点中,并从父节点中删除右节点对应的键值对,更新左侧节点的nextpageid。
  5. 若当前节点不为当前兄弟节点集合中的最右侧节点,我们对其右侧节点进行判断:1、若右侧节点的 size 大于 minsize ,我们将右侧节点的最左侧键值对加入左侧节点中,并更新当前节点的在父节点中的键;2、若右侧节点的 size 小于等于 minsize ,我们将左侧节点和右侧节点进行合并,将右侧节点中的所有键值对加入左侧节点中,并从父节点中删除右节点对应的键值对,更新左侧节点的nextpageid。
  6. 当合并的节点为内部节点,我们需要更新当前新节点的子节点父指针,并删除当前节点的父节点中的相应键值,并递归向上检查父节点是否合法。

总结

  1. 在进行操作时,需要区分内部节点和叶子节点,主要体现在内部节点和叶子节点中的键值对不同以及储存的内容不同:在叶子节点中我们主要储存实际的键值对,故插入删除时只需要执行相应操作即可;但在内部节点中,我们需要储存划分区间的键以及指向各子节点的 page id ,因此当子节点发生分裂、借值、合并时,我们都需要对父节点中的划分区间的键以及 page id 进行调整。
  2. 我们在插入或删除时,需要递归向上检查父节点在修改之后是否合法。值得注意的是,当我们最终检查到根节点时,执行的判断和内部节点不同。由于根节点不受 minsize 的限制,因此我们在插入时可以创建新的根节点,让当前层的节点全部指向新的根节点;当我们删除影响到根节点时,我们可以将下一层的节点作为新的根节点或者直接删除整棵树。

3、任务 3 - 索引迭代器

我们根据对应的要求实现索引迭代器的相应功能即可,主要体现在以下几功能:1、根据当前叶子节点的 next page id 判断是否指向叶子节点的末尾且当前序号是否为当前节点的最大值;2、根据索引返回对应的键值对;3、当序号为指向末端且GetNextPageId指向INVALID_PAGE_ID时,说明我们已经完成访问当前叶子节点,我们需要获得下一个节点的页面并转化未叶子节点类型,并充值序号,否则只需要序号加一即可;4、判断当前的迭代器与给定的迭代器在 page id 和 序号上是否相同,或当前叶子节点为空。

4、任务 4 - 并发索引

为了实现多线程的并发控制,我们在此处使用蟹行协议,其内容如下:

遍历索引的线程将获取然后释放 B+Tree 页面上的 latch。一个线程只能在它的子页面被认为是“安全的”时释放父页面上的 latch。注意,“安全”的定义可能会根据线程正在执行的操作的类型而变化。

我们在执行不同操作时,判断安全的依据也是不同的。同时项目中已经实现了transaction,我们可以从中获得 PageSet 用于记录我们在当前遍历过程中保持所有上锁页面的队列。

  1. 在执行查询操作时,我们只需要上读锁即可。由于我们不涉及写操作,因此我们从根节点开始进行遍历时,先给父节点上锁,在获得子节点之后,我们再给子节点上锁,此时我们可以认为当前子节点已经是安全的,而后释放父节点的锁。如此循环直至我们获得最终的叶子节点为止。
  2. 在执行插入操作时,我们同样从根节点开始遍历。考虑到我们需要对节点中的数组进行修改,故我们此时需要上写锁。我们同样先给父节点上写锁,而后给子节点上写锁,如此进行循环。此时有两种特殊情况可以让我们提前释放写锁:1、当我们获得的新内部节点的 size 小于 maxsize 时,此时即使再插入新节点也不会影响之前节点的状态,我们可以认为当前的节点和之前的节点都是安全的。故我们可以使用先前占用的所有节点对应的页面并不需要执行写操作;2、当我们获得的当我们获得的新叶子节点的 size 小于 maxsize-1 时,此时即使再插入新键值对也不会影响之前节点的状态,我们可以认为当前的节点和之前的节点都是安全的。故我们可以使用先前占用的所有节点对应的页面并不需要执行写操作。在最终执行完所有的插入操作之后,我们需要释放所有锁并对应解除页面的进程占用。
  3. 在执行删除操作时,我们同样从根节点开始遍历。考虑到我们需要对节点中的数组进行修改,故我们此时需要上写锁。我们同样先给父节点上写锁,而后给子节点上写锁,如此进行循环。此时有两种特殊情况可以让我提前释放锁:1、当当前节点为根节点且 size 大于 2 时,即使后续操作导致根节点需要删除,仍然可以保持不需要调整根节点,故可以直接释放;2、当当前节点为内部节点且 size 大于 minsize 时,此时即使我们删除了当前节点中的键值对仍不会导致出现借值或合并,故可以直接释放锁。

总结

  1. 使用transaction管理被占用的页面有如下好处,由于我们在向下进行遍历时不断加入新页面,因此当我们处理完叶子节点向上递归时,我们同样可以直接获得页面而不需要重复获取;
  2. 在执行插入操作时,我们可能会申请新的页面,但在获得新页面之后,由于新页面未连接至树中且未被别的线程访问,故可以不用加入占用的页面集合中,只需要解除线程使用即可;
  3. 在执行删除操作时,我们可能会使用到相邻节点的页面,因此在使用相关资源时需要对节点进行上锁,但我们在使用完成之后便可以直接释放,只需要保持当前节点的锁不释放即可,最终在完成删除操作之后再进行同意释放。
  4. 向下递归路径上的 page 需要全程持有(除非节点安全,提前释放),在整个操作完成后统一释放。其余 page 要么是重复获取,要么是暂时获取。重复获取无需加锁,使用完后直接 unpin。暂时获取(steal / merge / sibling)需要加锁,使用完后 unlatch & unpin。
  5. 不考虑迭代器时,由于我们遍历节点获得锁的方向都是相同的,故不可能发生死锁。但考虑迭代器后,可能在遍历迭代器时发生死锁,当迭代器在叶子节点从左向右进行遍历;而叶子节点中存在节点从右向左需要进行借值或合并时,便可能会发生死锁。此时需要规定迭代器无法获取锁时主动放弃。
  6. 考虑到蟹行协议对B+树中上层节点上锁会导致上层节点的锁竞争激烈,故我们可以对蟹行协议进行优化。我们首先假设不会发生分裂、借值、合并,我们在向下访问时仅使用写锁并即使释放:1、若最终不进行分裂、借值、合并时,我们可以直接完成操作;2、若最终会进行分裂、借值、合并时,我们放弃当前持有的所有锁,并从根节点开始重新机型访问。在此次过程上我们对于所有节点搜上写锁。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值