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>
-
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 beGenericKey
, the actual size ofGenericKey
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 twoKeyType
instances are less/greater-than each other. These will be included in theKeyType
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 equaloperator!=()
: 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
- 你必須使用傳入的transaction,把已經加鎖的頁面儲存起來。
- 我們提供了讀寫鎖存器的實現(
src / include / common / rwlatch.h
)。 並且已經在頁面標頭檔案下新增了輔助函式來獲取和釋放Latch鎖(src / include / storage / page / page.h
)。
2. Insert實現
首先附上書上的b+樹插入演算法
對上面幾種情況的分析
-
如果當前為空樹則建立一個葉子結點並且也是根節點
-- 這裡是
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(); }
-
否則尋找插入元素應該在的葉子結點
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_); }
-
分裂的步驟
-
呼叫
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; }
-
在
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的截圖完成第一部分✅