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的併發問題,所以對於加鎖、解鎖和事物都沒有考慮。
- [**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的截圖完成第一