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

vue中computed/method/watch的区别

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/

0. 写在前面

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

1. 实验介绍

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

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

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

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()。

记一次Apache的代码导致生产服务耗时增加

您必须在指定的文件中实现索引迭代器。 仅允许您修改头文件(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+树插入算法

对上面几种情况的分析

  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之后

    判断该元素是否已经在树中

    b. 进行分裂

    INDEX_TEMPLATE_ARGUMENTS
    bool BPLUSTREE_TYPE::InsertIntoLeaf(const KeyType &key, const ValueType &value, Transaction *transaction) {
      auto leaf = reinterpret_cast<BPlusTreeLeafPage<KeyType, ValueType,KeyComparator> *>(FindLeafPage(key, false,Operation::INSERT, transaction));
      if (leaf == nullptr) {
        return false;
      }
    
      // 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_);
      }
    
  3. 分裂的步骤

  4. 调用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;
    }
    
  5. 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 {
      page_id_t page_id;
      auto new_page = buffer_pool_manager_->NewPage(&page_id);
      if (new_page == nullptr) {
        throw "no page can used while InsertIntoParent";
      }
      auto virtual_node =reinterpret_cast<BPlusTreeInternalPage<KeyType, page_id_t,KeyComparator> *>(new_page->GetData());
      virtual_node->Init(page_id,old_node->GetParentPageId(),internal_max_size_);
      virtual_node->SetSize(internal->GetSize());
      for (int i = 1, j = 0; i <=internal->GetSize(); i++,j++) {
        if (internal->ValueAt(i-1) == old_node->GetPageId()) {
          virtual_node->SetKeyAt(j, key);
          virtual_node->SetValueAt(j, new_node->GetPageId());
          j++;
        }
        if (i < internal->GetSize()) {
          virtual_node->SetKeyAt(j, internal->KeyAt(i));
          virtual_node->SetValueAt(j, internal->ValueAt(i));
        }
      }
      assert(virtual_node->GetSize() == virtual_node->GetMaxSize());
      auto new_internal =Split<BPlusTreeInternalPage<KeyType, page_id_t, KeyComparator>>(virtual_node);

      internal->SetSize(virtual_node->GetSize() + 1);
      for (int i = 0; i < virtual_node->GetSize(); ++i) {
        internal->SetKeyAt(i + 1, virtual_node->KeyAt(i));
        internal->SetValueAt(i + 1, virtual_node->ValueAt(i));
      }

      // set new node parent page id
      if (comparator_(key, new_internal->KeyAt(0)) < 0) {
        new_node->SetParentPageId(internal->GetPageId());
      } else if (comparator_(key, new_internal->KeyAt(0)) == 0) {
        new_node->SetParentPageId(new_internal->GetPageId());
      } else {
        new_node->SetParentPageId(new_internal->GetPageId());
        old_node->SetParentPageId(new_internal->GetPageId());
      }

      // TODO unpin and delete virtual page
      buffer_pool_manager_->UnpinPage(new_node->GetPageId(), true);
      buffer_pool_manager_->UnpinPage(virtual_node->GetPageId(), false);
      buffer_pool_manager_->DeletePage(virtual_node->GetPageId());

      InsertIntoParent(internal, new_internal->KeyAt(0), new_internal);
    }

    buffer_pool_manager_->UnpinPage(internal->GetPageId(), true);
  }
}

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

附上一个pass的截图完成第一部分

配置 containerd 镜像仓库完全攻略

相关推荐

发表评论

路人甲

网友评论(0)