当前位置 博文首页 > 周小伦:CMU数据库(15-445)实验2-b+树索引实现(上)

    周小伦:CMU数据库(15-445)实验2-b+树索引实现(上)

    作者:周小伦 时间:2021-01-30 19:07

    Lab2

    在做实验2之前请确保实验1结果的正确性。不然你的实验2将无法正常进行

    环境搭建地址如下 https://www.cnblogs.com/JayL-zxl/p/14307260.html

    实验一的地址如下 https://www.cnblogs.com/JayL-zxl/p/14311883.html

    实验的地址如下 https://15445.courses.cs.cmu.edu/fall2020/project2/

    GitHub代码地址 https://github.com/JayL-zxl/CMU15-445Lab

    0. 写在前面

    Lab2真的好难写啊。写了好几天(虽然中间有回家、做核酸、出去玩。。各种的事情)但还算是写完了。真的参考了好多代码(这里建议大家有问题还是Google),最后勉强写完了真的不容易,下面记录一下我实验的过程。(写的超烂)

    1. 实验介绍

    第一个打分点---实现b+树的基本结构、插入、搜索操作

    注意这里没有考虑打分点2的并发问题,所以对于加锁、解锁和事物都没有考虑。

    • Task #1 - B+Tree Pages
    • Task #2.a - B+Tree Data Structure (Insertion & Point Search)

    第二个打分点--实现b+树的删除操作、索引迭代器和对并发访问的支持

    • Task #2.b - B+Tree Data Structure (Deletion)
    • Task #3 - Index Iterator
    • Task #4 - Concurrent Index

    Task 1 B+TREE PAGES

    您需要实现三个页面类来存储B+树的数据。

    • B+ Tree Parent Page
    • B+ Tree Internal Page
    • B+ Tree Leaf Page
    1. B+ Tree Parent Page

    这是内部页和叶页都继承的父类,它只包含两个子类共享的信息。父页面被划分为如下表所示的几个字段。

    B+Tree Parent Page Content

    Variable Name Size Description
    page_type_ 4 Page Type (internal or leaf)
    lsn_ 4 Log sequence number (Used in Project 4)
    size_ 4 Number of Key & Value pairs in page
    max_size_ 4 Max number of Key & Value pairs in page
    parent_page_id_ 4 Parent Page Id
    page_id_ 4 Self Page Id

    您必须在指定的文件中实现您的父页。您只能修改头文件(src/include/storage/page/b_plus_tree_page.h) 和其对应的源文件 (src/storage/page/b_plus_tree_page.cpp).

    2. B+TREE INTERNAL PAGE

    内部页不存储任何实际数据,而是存储有序的m个键条目和m + 1个指针(也称为page_id)。 由于指针的数量不等于键的数量,因此将第一个键设置为无效,并且查找方法应始终从第二个键开始。 任何时候,每个内部页面至少有一半已满。 在删除期间,可以将两个半满页面合并为合法页面,或者可以将其重新分配以避免合并,而在插入期间,可以将一个完整页面分为两部分。

    你只能修改头文件(src/include/storage/page/b_plus_tree_internal_page.h) 和对应的源文件(src/page/b_plus_tree_internal_page.cpp).

    * Internal page format (keys are stored in increasing order):
    *  --------------------------------------------------------------------------
    * | HEADER | KEY(1)+PAGE_ID(1) | KEY(2)+PAGE_ID(2) | ... | KEY(n)+PAGE_ID(n) |
    *  --------------------------------------------------------------------------
    #define INDEX_TEMPLATE_ARGUMENTS template <typename KeyType, typename ValueType, typename KeyComparat>
    
    1. B+TREE LEAF PAGE

    叶子页存储有序的m个键条目(key)和m个值条目(value)。 在您的实现中,值只能是用于定位实际元组存储位置的64位record_id,请参阅src / include / common / rid.h中定义的RID类。 叶子页与内部页在键/值对的数量上具有相同的限制,并且应该遵循相同的合并,重新分配和拆分操作。您必须在指定的文件中实现内部页。 仅允许您修改头文件(src / include / storage / page / b_plus_tree_leaf_page.h)及其相应的源文件(src / storage / page / b_plus_tree_leaf_page.cpp)。

    重要提示:尽管叶子页和内部页包含相同类型的键,但它们可能具有不同类型的值,因此叶子页和内部页的最大大小可能不同。每个B + Tree叶子/内部页面对应从缓冲池获取的存储页面的内容(即data_部分)。 因此,每次尝试读取或写入叶子/内部页面时,都需要首先使用其唯一的page_id从缓冲池中提取页面,然后将其重新解释为叶子或内部页面,并在写入或删除后执行unpin操作。

    Task 2.A - B+TREE DATA STRUCTURE (INSERTION & POINT SEARCH)

    您的B +树索引只能支持唯一键。 也就是说,当您尝试将具有重复键的键值对插入索引时,它应该返回false

    对于checkpoint1,仅需要B + Tree索引支持插入(Insert)和点搜索(GetValue)。 您不需要实现删除操作。 插入后如果当前键/值对的数量等于max_size,则应该正确执行分割。 由于任何写操作都可能导致B + Tree索引中的root_page_id发生更改,因此您有责任更新(src / include / storage / page / header_page.h)中的root_page_id,以确保索引在磁盘上具有持久性 。 在BPlusTree类中,我们已经为您实现了一个名为UpdateRootPageId的函数。 您需要做的就是在B + Tree索引的root_page_id更改时调用此函数。

    您的B + Tree实现必须隐藏key/value等的详细信息,建议使用如下结构:

    template <typename KeyType,
              typename ValueType,
              typename KeyComparator>
    class BPlusTree{
       // ---
    };
    

    这些类别已经为你实现了

    • KeyType: The type of each key in the index. This will only be GenericKey, the actual size of GenericKey is specified and instantiated with a template argument and depends on the data type of indexed attribute.

    • ValueType: The type of each value in the index. This will only be 64-bit RID.

    • KeyComparator: The class used to compare whether two KeyType instances are less/greater-than each other. These will be included in the KeyType implementation files.

    TASK #2.B - B+TREE DATA STRUCTURE (DELETION)

    您的B+树索引需要支持删除。如果删除导致某些页面低于占用阈值,那么您的B+树索引应该正确执行合并或重新分配。同样,您的B+树索引只能支持唯一键

    TASK #3 - INDEX ITERATOR

    您将构建一个通用索引迭代器,以有效地检索所有叶子页面。 基本思想是将它们组织到一个链接列表中,然后按照B + Tree叶子页中存储的特定方向遍历每个键/值对。 您的索引迭代器应遵循C ++ 17中定义的迭代器功能,包括使用一组运算符对一系列元素进行迭代的能力,以及for-each循环(至少具有++,==,!=和解引用运算符)。 请注意为了支持索引的每个循环功能,您的BPlusTree应该正确实现begin()和end()。

    您必须在指定的文件中实现索引迭代器。 仅允许您修改头文件(src / include / storage / index / index_iterator.h)及其相应的源文件(src / index / storage / index_iterator.cpp)。 您不需要修改任何其他文件。 您必须在这些文件中的IndexIterator类中实现以下功能。 在索引迭代器的实现中,只要您具有以下三种方法,就可以添加任何帮助程序方法。

    • isEnd(): Return whether this iterator is pointing at the last key/value pair.
    • operator++(): Move to the next key/value pair.
    • operator*(): Return the key/value pair this iterator is currently pointing at.
    • operator==(): Return whether two iterators are equal
    • operator!=(): Return whether two iterators are not equal.

    TASK #4 - CONCURRENT INDEX

    在这一部分中,您需要更新原始的单线程B + Tree索引,以便它可以支持并发操作。 我们将使用课堂和教科书中介绍的Latch捕捉技术。 遍历索引的线程将获取然后释放B + Tree页上的Latch锁。 如果线程的子页面被认为是“安全的”,则该线程只能释放其父页面上的Latch锁。 请注意,“安全”的定义可能会根据线程执行的操作类型而有所不同:

    • Search: Starting with root page, grab read (R) latch on child Then release latch on parent as soon as you land on the child page.
    • Insert: Starting with root page, grab write (W) latch on child. Once child is locked, check if it is safe, in this case, not full. If child is safe, release all locks on ancestors.
    • Delete: Starting with root page, grab write (W) latch on child. Once child is locked, check if it is safe, in this case, at least half-full. (NOTE: for root page, we need to check with different standards) If child is safe, release all locks on ancestors.

    Hints

    1. 你必须使用传入的transaction,把已经加锁的页面保存起来。
    2. 我们提供了读写锁存器的实现(src / include / common / rwlatch.h)。 并且已经在页面头文件下添加了辅助函数来获取和释放Latch锁(src / include / storage / page / page.h)。

    2. Insert实现

    首先附上书上的b+树插入算法

    image-20210124184901398

    对上面几种情况的分析

    1. 如果当前为空树则创建一个叶子结点并且也是根节点

      -- 这里是leaf结点所以这里需要用到leaf page内的函数

      -- 注意这里需要用lab1实现的buffer池管理器来获得page。 这里记得创建完新的结点之后要unpin

      -- 进行插入的时候用二分插入来进行优化

      • 创建新结点
      INDEX_TEMPLATE_ARGUMENTS
      void BPLUSTREE_TYPE::StartNewTree(const KeyType &key, const ValueType &value) {
        auto page = buffer_pool_manager_->NewPage(&root_page_id_);
        if (page == nullptr) {
          throw "all page are pinned";
        }
        auto root =reinterpret_cast<BPlusTreeLeafPage<KeyType, ValueType,KeyComparator> *>(page->GetData());
        UpdateRootPageId(true);
        root->Init(root_page_id_, INVALID_PAGE_ID ,leaf_max_size_);
        root->Insert(key, value, comparator_);
        // unpin
        buffer_pool_manager_->UnpinPage(root->GetPageId(), true);
      }
      
      • insert函数
      /*
      in b_plus_leaf_page.h
      */
      INDEX_TEMPLATE_ARGUMENTS
      int B_PLUS_TREE_LEAF_PAGE_TYPE::Insert(const KeyType &key, const ValueType &value, const KeyComparator &comparator) {
        if(!GetSize()||comparator(key, KeyAt(GetSize() - 1)) > 0) array[GetSize()] = {key, value};
          else{
            int l=0,r=GetSize()-1;
            while(l<r){
              int mid=(l+r)>>1;
              if(comparator(key,array[mid].first)<0)r=mid;
              else if(comparator(key,array[mid].first)>0)l=mid+1;
              else assert(0);
            }
            memmove(array + r + 1, array + r,static_cast<size_t>((GetSize() - r)*sizeof(MappingType)));
            array[r] = {key, value};
          }
        IncreaseSize(1);
        return GetSize();
      }
      
    2. 否则寻找应该包含插入元素的叶子结点
      -- 首先找到叶子结点
      -- 如果叶子结点内的元素个数小于最大值则直接插入
      -- 否则需要进行分裂。产生两个新的结点。把元素上提
      -- 如果提到父亲结点,父结点仍需要分裂。则递归进行分裂否则结束

      a . 如果叶子结点内的关键字小于m-1,则直接插入到叶子结点insert_into_leaf

      • 利用findLeafPage函数找到叶子结点

      要考虑无论是读或者写从根节点。到叶子结点都需要加锁。然后注意释放锁否则会锁死。(这个地方测试的时候卡死了我好久)

      这里对原来的函数定义做了一些修改。加入了操作类型的判断。

      /*
      定义在b_plus_tree.h中
      定义方法和定义page类型保持一致
      */
      enum class Operation { READ = 0, INSERT, DELETE };
      
      INDEX_TEMPLATE_ARGUMENTS
      Page *BPlusTree<KeyType, ValueType, KeyComparator>::FindLeafPage(const KeyType &key, bool leftMost, Operation op, Transaction *transaction) {
      
        if (IsEmpty()) {
          return nullptr;
        }
        auto root = buffer_pool_manager_->FetchPage(root_page_id_);
        if (root == nullptr) {
          throw "no page can find";
        }
      
        if (op == Operation::READ) {
          root->RLatch();
        } else {
          root->WLatch();
        }
        if (transaction != nullptr) {
          transaction->AddIntoPageSet(root);
        }
      
        auto node = reinterpret_cast<BPlusTreePage *>(root->GetData());
        while (!node->IsLeafPage()) {
          auto internal =reinterpret_cast<BPlusTreeInternalPage<KeyType, page_id_t,KeyComparator> *>(node);
          page_id_t parent_page_id = node->GetPageId(), child_page_id;
          if (leftMost) {
            child_page_id = internal->ValueAt(0);
          } else {
            child_page_id = internal->Lookup(key, comparator_);
          }
          auto child = buffer_pool_manager_->FetchPage(child_page_id);
          if (child == nullptr) {
            throw "not find child in findLeaf";
          }
      
          if (op == Operation::READ) {
            child->RLatch();
            UnlockUnpinPages(op, transaction);
          } else {
            child->WLatch();
          }
          node = reinterpret_cast<BPlusTreePage *>(child->GetData());
          assert(node->GetParentPageId() == parent_page_id);
          // child is locked, If child is safe, release all locks on ancestors.
          if (op != Operation::READ && isSafe(node, op)) {
            UnlockUnpinPages(op, transaction);
          }
          if (transaction != nullptr) {
            transaction->AddIntoPageSet(child);
          } else {
            root->RUnlatch();
            buffer_pool_manager_->UnpinPage(root->GetPageId(), false);
            root = child;
          }
        }
        return reinterpret_cast<Page*>(node);
      }
      
      • Lookup函数

      找到key值所在的page---二分查找

      INDEX_TEMPLATE_ARGUMENTS
      ValueType B_PLUS_TREE_INTERNAL_PAGE_TYPE::Lookup(const KeyType &key, const KeyComparator &comparator) const {
        int l=0,r=GetSize()-1;
        if (comparator(key, array[1].first) < 0) return array[0].second;
        else{
          while(l<r){
            int mid=(l+r)>>1;
            if(comparator(key,array[mid].first)<0)r=mid;
            else if(comparator(key, array[mid].first) > 0) l=mid+1;
            else return array[mid].second;
          }
        }
        return array[r].second;
      }
      
      • 找到Leaf page之后

      判断该元素是否已经在树中。如果该元素当前不在树中则调用insert函数直接插入

       // if already in the tree, return false
       ValueType v;
       if (leaf->Lookup(key, &v, comparator_)) {
         UnlockUnpinPages(Operation::INSERT, transaction);
         return false;
       }
      //case 1  keys in leaf page <m-1
      if (leaf->GetSize() < leaf->GetMaxSize()) {
      leaf->Insert(key, value, comparator_);
      }
      

    b. 进行分裂

    1. 分裂的步骤

    2. 调用split函数对叶子结点进行分割

      --- split的时候会产生一个含有m-m/2个关键字的新结点。注意把两个叶子结点连接起来。

      --- 然后调用InsertIntoParent

       // case 2 need to split
        else {
          leaf->Insert(key, value, comparator_);
          auto new_leaf = Split<BPlusTreeLeafPage<KeyType, ValueType, KeyComparator>>(leaf);
          new_leaf->SetNextPageId(leaf->GetNextPageId());
          leaf->SetNextPageId(new_leaf->GetPageId());
      
          // insert the split key into parent
          InsertIntoParent(leaf, new_leaf->KeyAt(0), new_leaf, transaction);
        }
        UnlockUnpinPages(Operation::INSERT, transaction);
      
        return true;
      
      }
      
    3. InsertIntoParent

      case1-- 如果当前结点为根节点。则创建一个新的根节点。新根节点的子结点为分裂所得(经过split操作后)得到的两个结点

      INDEX_TEMPLATE_ARGUMENTS
      void BPLUSTREE_TYPE::InsertIntoParent(BPlusTreePage *old_node, const KeyType &key, BPlusTreePage *new_node,Transaction *transaction) {
        //case 1 create new root
        if (old_node->IsRootPage()) {
          auto page = buffer_pool_manager_->NewPage(&root_page_id_);
          if (page == nullptr) {
            throw "not page can used in  InsertIntoParent";
          }
          assert(page->GetPinCount() == 1);
          auto root =reinterpret_cast<BPlusTreeInternalPage<KeyType, page_id_t,KeyComparator> *>(page->GetData());
          root->Init(root_page_id_,INVALID_PAGE_ID,internal_max_size_);
          root->PopulateNewRoot(old_node->GetPageId(), key, new_node->GetPageId());
      
          old_node->SetParentPageId(root_page_id_);
          new_node->SetParentPageId(root_page_id_);
      
          //TODO update to new root_page_id
          UpdateRootPageId(false);
          //TODO unpin
          buffer_pool_manager_->UnpinPage(new_node->GetPageId(), true);
          buffer_pool_manager_->UnpinPage(root->GetPageId(), true);
      
        }
      

      case2 -- 否则要递归上述的过程

      a. 先找分裂产生结点的父亲结点。如果可以直接插入则直接插入

      b. 否则需要对父结点在进行分裂,即递归调用。

      //case2  insert into parent
      else {
        auto parent_page = buffer_pool_manager_->FetchPage(old_node->GetParentPageId());
        if (parent_page == nullptr) {
          throw "no old_node parent page can used";
        }
        auto internal =reinterpret_cast<BPlusTreeInternalPage<KeyType, page_id_t,KeyComparator> *>(parent_page->GetData());
        // case 2.a insert directly
        if (internal->GetSize() < internal->GetMaxSize()) {
          internal->InsertNodeAfter(old_node->GetPageId(), key, new_node->GetPageId());
          new_node->SetParentPageId(internal->GetPageId());
          buffer_pool_manager_->UnpinPage(new_node->GetPageId(), true);
        }
        //case 2.b the parent node need to split
        else {
            internal->InsertNodeAfter(old_node->GetPageId(), key, new_node->GetPageId());
           auto new_internal =Split<BPlusTreeInternalPage<KeyType, page_id_t, KeyComparator>>(internal);
           new_internal->SetMaxSize(internal_max_size_);
    
           // TODO unpin and delete virtual page
           buffer_pool_manager_->UnpinPage(new_node->GetPageId(), true);
           buffer_pool_manager_->UnpinPage(internal->GetPageId(), false);
           InsertIntoParent(internal, new_internal->KeyAt(0), new_internal);
        }
    
        buffer_pool_manager_->UnpinPage(internal->GetPageId(), true);
      }
    }
    

    好了实验2的第一部分就到这里了。整个实验都已经写完啦。剩下就是优化代码,写博客记录了,所以实验2的第二部分也会很快更新的。这里面的代码不是很详细。等到第二部分写完之后,会一整个完全上传到GitHub上的。

    image-20210124230210652

    附上一个pass的截图完成第一部分?
    如果我们插入10、15、16、17、19、20那么我们用程序得到的结果如下
    image-20210124230210652
    可以发现是完全正确的 ??

    GitHub代码地址 https://github.com/JayL-zxl/CMU15-445Lab

    bk