P2 B+树索引

文章目录

    • Task1 B+树页
      • B+树页
      • B+树内部结点
      • B+树叶子结点
    • Task2 B+树操作
      • Task2 B+树插入和搜索的单一值
        • 插入单一值
        • 搜索单一值
      • Task2 B+树删除
    • Task3 叶子扫描的迭代器
    • Task4 并行索引

Task1 B+树页

B+树页

实际上是每个B+树页面的标题部分,包含叶子页面和内部页面共享的信息。

报头格式(大小以字节为单位,共12字节):
| PageType (4) | CurrentSize (4) | MaxSize (4) |

// define page type enum
enum class IndexPageType { INVALID_INDEX_PAGE = 0, LEAF_PAGE, INTERNAL_PAGE };
// 成员变量,内部页和叶页共享的属性
IndexPageType page_type_ __attribute__((__unused__));
int size_ __attribute__((__unused__));      // 键值对的数量,内部页的第一个键为无效值
int max_size_ __attribute__((__unused__));  // 键值对的容量

B+树内部结点

在内部页面中存储n个索引键和n+1个子指针(page_id)。

指针PAGE_ID(i)指向一个子树,其中所有键K满足:K(i) <= K < K(i+1)。

注意:由于键的数量不等于子指针的数量,所以第一个键总是无效的。也就是说,任何搜索/查找都应该忽略第一个键。

内部页面格式(键按递增顺序存储):
| HEADER | KEY(1)+PAGE_ID(1) | KEY(2)+PAGE_ID(2) | ... | KEY(n)+PAGE_ID(n) |

#define MappingType std::pair<KeyType, ValueType>
//key -> page_id 的映射
MappingType array_[0];

B+树叶子结点

只支持唯一键。

叶页格式(键按顺序存储):
| HEADER | KEY(1) + RID(1) | KEY(2) + RID(2) | ... | KEY(n) + RID(n)

 HEADER格式(大小为字节,共16字节):
| PageType (4) | CurrentSize (4) | MaxSize (4) |  NextPageId (4)

page_id_t next_page_id_;  // 先定位到一个叶子,再顺序向后扫描
//key->record_id 的映射,record id = page id combined with slot id
MappingType array_[0];

Task2 B+树操作

大部分操作在b_plus_tree.cpp实现

Context类用于记录操作B+树过程中需要记忆的路径、读写保护等

class Context {
    public:
    // 当您插入/删除B+树时,将头页的写保护存储在这里。
    // 当您想要解锁所有内容时,请记住取消标题页保护并将其设置为空值,即header_page.reset()
    std::optional<WritePageGuard> header_page_{std::nullopt};

    //在这里保存根页面id,以便更容易地知道当前页面是否是根页面。
    page_id_t root_page_id_{INVALID_PAGE_ID};

    // 要修改的页面的写保护的队列。
    std::deque<WritePageGuard> write_set_;

    // 读保护队列
    std::deque<ReadPageGuard> read_set_;

    auto IsRootPage(page_id_t page_id) -> bool { return page_id == root_page_id_; }
};

Task2 B+树插入和搜索的单一值

插入单一值

BPLUSTREE_TYPE::Insert()实现,若BPLUSTREE_TYPE::header_page_id_属性标识的根节点的页(BPlusTreeHeaderPage)的根节点页id无效,说明没有根页面,调用缓冲区的NewPage()新建root页,然后强转为BPlusTreeLeafPage类型,此时根节点就是叶子节点,然后BPlusTreeLeafPage::InsertAtLast()根叶后插k-v,标识正在使用该页,清空header_page_(检查header_page_id_标识的根节点页时,页就存在这里)。若有根节点就获取根节点页id,调用InsertIntoLeaf()

BPLUSTREE_TYPE::InsertIntoLeaf()开头就调用了BPLUSTREE_TYPE::FindLeaf(),此函数的作用是为插入/删除结点操作寻找目标叶,结果存在B+树类的Context类成员的write_set_中。回到函数,取出查找到的页,强转为BPlusTreeLeafPage后直接调用BPlusTreeLeafPage::Insert()插入k-v,若插入失败说明是重复键值对,清空header_page_write_set_后退出,并且若叶页大小没超过叶子最大容量则此插入是安全插入,清空header_page_write_set_后退出,剩下的情况就是叶页满了,需要分裂,调用Split()进行分裂,返回的新页引用是满页分裂出的兄弟叶页,之后调用InsertIntoParent()修改父结点结构。

BPLUSTREE_TYPE::InsertIntoParent()是个直接递归,在函数开头有个判断write_set_内的锁有一个时,这些锁是在InsertIntoLeaf()中调用FindLeaf()时产生的,表示修改该叶子会导致write_set_中的这些结点页被修改,即最接近叶子的安全结点到叶子节点这一路径上的所有结点的锁。在write_set_中只剩一个页时,NewPage()新建一个页,当作新根节点,然后旧根节点和分裂出的新结点做新根的子节点,返回。通过write_set_获取父结点,然后调用BPlusTreeInternalPage::InsertAfterValue()将分裂后产生的新节点插入父节点,write_set_弹出旧结点(原来不安全的结点)。当父节点的大小超过了最大容量,则父节点不安全,调用Split()分裂并调用InsertIntoParent()递归,若是安全结点,则清理header_page_write_set_,结束。

BPLUSTREE_TYPE::Split()分裂结点,返回新的兄弟页。NewPage()创建新页,叶子节点分裂就将新页和待分裂的页强转为叶页,初始化新叶页后旧叶页调用BPlusTreeLeafPage::SplitLeafTo(),取出旧叶页的后一半数据给新叶页,并且设置旧叶页和新叶页的next_page_id。最后设置正在使用新叶页,然后返回。若分裂内部节点,将新页和待分裂的页强转为内部结点页,新结点页初始化后旧结点页调用BPlusTreeInternalPage::SplitInternalTo()取内部节点的最大容量加一的一半(后半段)的内容给新内部节点,限定结点大小。设置新结点页正在使用后返回。

这里我不理解的是在InsertIntoParent()中仅仅用if (ctx.write_set_.size() == 1)就确定是根节点分裂

搜索单一值

BPLUSTREE_TYPE::GetValue()实现,获取头部页的写锁header_page_,通过它获得根节点id,将根节点读锁加入read_set_然后强转为BPlusTreePage,清空header_page_,强转结点为BPlusTreeInternalPage,调用BPlusTreeInternalPage::FindInternelKey()按key二分查找中间节点的子节点引用,查到的页加读锁,加入read_set_然后将原先在read_set_中的父页弹出,这样就实现了读锁的"下沉",之后进入循环直到页是叶页。循环出来后将页强转为BPlusTreeLeafPage类,之后调用BPlusTreeLeafPage::FindKey()二分查找叶子节点的值,找到后组装传参vector。

Task2 B+树删除

BPLUSTREE_TYPE::Remove()实现,获取头部页写锁并得到根节点id,调用FindLeaf()查找key,获取查到的页强转成BPlusTreeLeafPage,调用BPlusTreeLeafPage::DeleteKey()先二分查找叶子结点key,若没有调用DeleteKeyAt()删除数组中对应的k-v,删除失败就清理header_page_write_set_并退出,若删除的k-v所在的叶页大小大于等于最小容量(叶子结点是max_size_/2,内部节点是(max_size_+1)/2)时,即为安全删除,清空write_set_并退出,若根节点时叶子,叶子根节点有一个键值对就能退出。最后都没有退出的就是需要非安全删除情况,可能会有修改父节点key、小于最小尺寸需从兄弟节点处拿甚至兄弟节点也不足直接合并,调用DealNode()合并或删除叶页。

BPLUSTREE_TYPE::DealNode()是个间接递归,它调用Merge()Merge()调用它。处理借取键值对或合并节点,write_set_的最后一个值是这个函数体需要处理的不安全的节点(已进行删除工作)。

  • 若是根节点

    • 根叶:清理write_set_header_page_
    • 不是叶:强转为内部节点,取第0个的引用做根节点id(header_page_强转为BPlusTreeHeaderPage可取root_page_id_

    返回

  • 不是根

    write_set_取出该页的父页,强转为内部节点

    • 是叶
      强转为叶页,父页调用FindInternelKey()二分查找叶子key获得index

      左边有值,有左兄弟(index > 0):取左兄弟(父页的index-1)的写锁存入write_set_,强转为叶页后保存变量

      右边有值,有右兄弟(index < 父页容量-1):取右兄弟(父页的index+1)的写锁存入write_set_,强转为叶页后保存变量

      • 左右兄弟都在(左叶页变量 && 右叶页变量):
        • 两个兄弟都不足(兄弟大小 <= 最小容量):调用Merge()与右节点合并,返回
        • 右边有剩余(右兄弟大小 > 最小容量):清空header_page_,向兄弟节点借键值对表示父页是安全节点,释放write_set_中父页的祖先节点及根的锁,叶页调用LendFromBrother()向右兄弟借,之后父页调用SetKeyAt()更新父页中的右兄弟结点在父页的索引,然后清理write_set_
        • 左边有剩余(else):清空header_page_,释放write_set_中父页的祖先节点及根的锁,叶页调用LendFromBrother()向左兄弟借,父页调用SetKeyAt()更新父页中的右兄弟结点在父页的索引,然后清理write_set_
      • 只有左兄弟(左叶页变量):
        • 左兄弟不足(左兄弟大小 <= 最小容量):调用Merge()与左兄弟合并,返回
        • 左边有剩余:清空header_set_,释放write_set_中父页祖先节点及根的锁,叶页调用LendFromBrother()向左兄弟借,父页调用SetKeyAt()更新父页中的左兄弟结点在父页的索引,然后清理write_set_
      • 只有右兄弟(右叶页变量):
        • 右兄弟不足(右兄弟大小 <= 最小容量):调用Merge()与右兄弟合并,返回
        • 右边有剩余:清空header_set_,释放write_set_中父页祖先节点及根的锁,叶页调用LendFromBrother()向右兄弟借,父页调用SetKeyAt()更新父页中的右兄弟结点在父页的索引,然后清理write_set_
    • 是内部结点

      强转为内部结点页,父页调用FindInternelKey()二分查找内部结点获得index

      左边有值,有左兄弟(index > 0):取左兄弟(父页的index-1)的写锁存入write_set_,强转为内部结点页后保存变量

      右边有值,有右兄弟(index < 父页容量-1):取右兄弟(父页的index+1)的写锁存入write_set_,强转为内部结点后保存变量

      • 左右兄弟都在(左结点页变量 && 右结点页变量):
        • 两个兄弟都不足(兄弟大小 <= 最小容量):调用Merge()与右节点合并,返回
        • 右边有剩余(右兄弟大小 > 最小容量):清空header_page_,释放write_set_中父页的祖先节点及根的锁,内部结点页调用LendFromBrother()向右兄弟借,之后父页调用SetKeyAt()更新父页中的右兄弟结点在父页的索引,然后清理write_set_
        • 左边有剩余(else):清空header_page_,释放write_set_中父页的祖先节点及根的锁,叶页调用LendFromBrother()向左兄弟借,父页调用SetKeyAt()更新父页中的右兄弟结点在父页的索引,然后清理write_set_
      • 只有左兄弟(左结点页变量):
        • 左兄弟不足(左兄弟大小 <= 最小容量):调用Merge()与左兄弟合并,返回
        • 左边有剩余:清空header_set_,释放write_set_中父页祖先节点及根的锁,叶页调用LendFromBrother()向左兄弟借,父页调用SetKeyAt()更新父页中的左兄弟结点在父页的索引,然后清理write_set_
      • 只有右兄弟(右结点页变量):
        • 右兄弟不足(右兄弟大小 <= 最小容量):调用Merge()与右兄弟合并,返回
        • 右边有剩余:清空header_set_,释放write_set_中父页祖先节点及根的锁,叶页调用LendFromBrother()向右兄弟借,父页调用SetKeyAt()更新父页中的右兄弟结点在父页的索引,然后清理write_set_

BPLUSTREE_TYPE::Merge()总是把右边节点的键值对转移给左边,再删除右边节点,这样方便更新NextPageId,若传入的经删除操作的页是叶子节点,将该页和兄弟页强转为叶页后,判断是右兄弟,兄弟结点就调用BPlusTreeLeafPage::MoveAllTo()将自己的k-v顺序插入该页的末尾,将该页的NextPageId设置为右兄弟的,设置删除结点索引为右兄弟索引(该页索引+1)。若判断是左兄弟,该页调用BPlusTreeLeafPage::MoveAllTo()将自己的k-v顺序插入左兄弟的尾部,左兄弟的NextPageId设置为该页的,设置删除节点索引为该页索引。若进行删除操作的页是内部结点,强转该页和兄弟页为内部节点页,判断是右兄弟就把兄弟k-v插入该页末尾,设置删除索引为兄弟,反之同理。之后将write_set_中该页父页的孩子出栈解锁,只剩父页和其祖先节点,把父页强转为内部节点页,调用DeleteKeyAt()将删除结点索引指定的结点页删除。若父页大小大于等于最小容量(安全删除),清理write_set_后就可返回,若是根结点,内部根节点右两个键值对(容量大于1)就行,返回,其他情况就是不安全删除,调用DealNode()进行间接递归。

BPlusTreeLeafPage::LendFromBrother()从brother移动一个键值对过来,兄弟调用KeyAt()ValueAt(),针对传入是否是左兄弟,将k-v插入结点的头或尾并设置兄弟的大小或删除k-v,最后返回新key。

Task3 叶子扫描的迭代器

必须添加一个c++迭代器,以有效地支持对叶页中的数据进行有序扫描。基本思想是存储兄弟指针,这样就可以有效地遍历叶页,然后实现一个迭代器,按顺序遍历每个叶页中的每个键值对。

Begin()End()函数在b_plus_true.cpp中,而迭代器类在index_iterator.cpp中实现

//索引迭代器类的私有属性
BPlusTreeLeafPage<KeyType, ValueType, KeyComparator> *leaf_page_{nullptr};  // 此迭代器指向的叶子
int index_{-1};  // 表示此迭代器现在指向leaf_page_中某叶页的第几个键值对
page_id_t my_page_id_;//表示迭代器指向的leaf_page_中某叶页的页id
BufferPoolManager *bpm_{nullptr};

重载了* ++ == !=四个符号来对索引迭代器进行操作

BPLUSTREE_TYPE::Begin()有重载,分别对应处理没给key和给了key的情况。没给就直接调用FindLeafPage()传入空key并指定找最左边的k-v,这样就返回了叶页链表首页,然后强转为叶页,构造索引迭代器后返回。传入key的Begin()多加了一层检测传回的页是否真的有key,没有说明FindLeafPage()没有找到,返回空索引迭代器。

BPLUSTREE_TYPE::End()同无参Begin()只不过指定找的是最右边的k-v(即index=internal_page->GetSize()-1

BPLUSTREE_TYPE::FindLeafPage()无锁获取根节点,根据传入的indextype判断需要的是最左/右的页还是进行k-v查找。

Task4 并行索引

见Task 2

你可能感兴趣的:(BusTub项目作业源码阅读,b树,c++,数据库,数据结构)