CMU資料庫(15-445)實驗2-b+樹索引實現(上)

周小倫發表於2021-01-25

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操作。

您的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之後

    判斷該元素是否已經在樹中

    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上的。

image-20210124230210652

附上一個pass的截圖完成第一部分✅

相關文章