1. 程式人生 > >CMU資料庫(15-445)實驗2-b+樹索引實現(上)

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

## 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的併發問題,所以對於加鎖、解鎖和事物都沒有考慮。 - [**Task #1 - B+Tree Pages**](https://15445.courses.cs.cmu.edu/fall2020/project2/#b+tree-pages) - [**Task #2.a - B+Tree Data Structure (Insertion & Point Search)**](https://15445.courses.cs.cmu.edu/fall2020/project2/#b+tree-structure-1) > 第二個打分點--實現b+樹的刪除操作、索引迭代器和對併發訪問的支援 - [**Task #2.b - B+Tree Data Structure (Deletion)**](https://15445.courses.cs.cmu.edu/fall2020/project2/#b+tree-structure-2) - [**Task #3 - Index Iterator**](https://15445.courses.cs.cmu.edu/fall2020/project2/#index-iterator) - [**Task #4 - Concurrent Index**](https://15445.courses.cs.cmu.edu/fall2020/project2/#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`). ```c++ * 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 ``` 3. ##### 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等的詳細資訊,建議使用如下結構: ```c++ template 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+樹插入演算法
**對上面幾種情況的分析** 1. **如果當前為空樹則建立一個葉子結點並且也是根節點** -- 這裡是`leaf`結點所以這裡需要用到`leaf page`內的函式 -- 注意這裡需要用lab1實現的buffer池管理器來獲得page。 這裡記得建立完新的結點之後要unpin -- 進行插入的時候用二分插入來進行優化 + 建立新結點 ```c++ 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 *>(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`函式 ```c++ /* 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>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((GetSize() - r)*sizeof(MappingType))); array[r] = {key, value}; } IncreaseSize(1); return GetSize(); } ``` 2. **否則尋找插入元素應該在的葉子結點** a . 如果葉子結點內的關鍵字小於m-1,則直接插入到葉子結點`insert_into_leaf` + `findLeafPage`函式有點複雜 > 要考慮無論是讀或者寫從根節點。到葉子結點都需要加鎖。然後注意釋放鎖否則會鎖死。(這個地方測試的時候卡死了我好久) 這裡對原來的函式定義做了一些修改。加入了操作型別的判斷。 ```c++ /* 定義在b_plus_tree.h中 定義方法和定義page型別保持一致 */ enum class Operation { READ = 0, INSERT, DELETE }; ``` ```c++ INDEX_TEMPLATE_ARGUMENTS Page *BPlusTree::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(root->GetData()); while (!node->IsLeafPage()) { auto internal =reinterpret_cast *>(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(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(node); } ``` + `Lookup`函式 > 找到key值所在的page---二分查詢 ```c++ 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>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. 進行分裂 ```c++ INDEX_TEMPLATE_ARGUMENTS bool BPLUSTREE_TYPE::InsertIntoLeaf(const KeyType &key, const ValueType &value, Transaction *transaction) { auto leaf = reinterpret_cast *>(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 GetSize() < leaf->GetMaxSize()) { leaf->Insert(key, value, comparator_); } ``` 3. **分裂的步驟** 4. 呼叫`split`函式對葉子結點進行分割 --- split的時候會產生一個含有m-m/2個關鍵字的新結點。注意把兩個葉子結點連線起來。 --- 然後呼叫`InsertIntoParent` ```c++ // case 2 need to split else { leaf->Insert(key, value, comparator_); auto new_leaf = Split>(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操作後)得到的兩個結點 ```c++ 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 *>(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. 否則需要分裂 ```c++ //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 *>(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 *>(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>(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的截圖完成第一