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. 寫在前面
嘿嘿周小倫重新把lab梳理一遍。同時修改一下之前出現的錯誤。和博客中不完善的地方。
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
).
這里都是一些簡單的set、get就不寫出來了
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>
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
)。
‼️重要的KeyIndex函數
這個函數可以返回第一個>=當前key值的編號。這個在插入的時候經常會用到,這樣就可以讓代碼重復利用,在LevelDB
中也有類似的操作。
INDEX_TEMPLATE_ARGUMENTS
int B_PLUS_TREE_LEAF_PAGE_TYPE::KeyIndex(const KeyType &key, const KeyComparator &comparator) const {
// 二分查找
// TODO : 換成std::lower_bound()好看一點
int l = 0;
int r = GetSize();
if (l >= r) {
return GetSize();
}
while (l < r) {
int mid = (l + r) / 2;
if (comparator(array_[mid].first, key) < 0) {
l = mid + 1;
} else {
r = mid;
}
}
return l;
}
重要提示:盡管葉子頁和內部頁包含相同類型的鍵,但它們可能具有不同類型的值,因此葉子頁和內部頁的最大大小可能不同。每個B + Tree
葉子/內部頁面對應從緩沖池獲取的存儲頁面的內容(即data_部分)。 因此,每次嘗試讀取或寫入葉子/內部頁面時,都需要首先使用其唯一的page_id從緩沖池中提取頁面,然后將其重新解釋為葉子或內部頁面,並在寫入或刪除后執行unpin
操作。
Task 2.A - B+TREE DATA STRUCTURE (INSERTION & POINT SEARCH)
其實就是實現
b_plus_tree.cpp/InsertIntoLeaf
函數所涉及到的相關函數。
您的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.
- 你必須使用傳入的transaction,把已經加鎖的頁面保存起來。
- 我們提供了讀寫鎖存器的實現(
src / include / common / rwlatch.h
)。 並且已經在頁面頭文件下添加了輔助函數來獲取和釋放Latch鎖(src / include / storage / page / page.h
)。
首先附上書上的b+樹插入算法

對上面幾種情況的分析
1. 如果當前為空樹則創建一個葉子結點並且也是根節點
-
這里是
leaf
結點所以這里需要用到leaf page
內的函數 -
注意這里需要用lab1實現的buffer池管理器來獲得page。 這里記得創建完新的結點之后要unpin
-
進行插入的時候用二分插入來進行優化
1. 創建新結點
INDEX_TEMPLATE_ARGUMENTS
void BPLUSTREE_TYPE::StartNewTree(const KeyType &key, const ValueType &value) {
Page *new_page = buffer_pool_manager_->NewPage(&root_page_id_);
if (new_page == nullptr) {
throw "out of memory";
}
LeafPage *leaf_page = reinterpret_cast<LeafPage *>(new_page);
leaf_page->Init(root_page_id_,INVALID_PAGE_ID,leaf_max_size_);
//update root page id
UpdateRootPageId(1);
// Insert entry directly into leaf page.
// For a new B+ tree, the root page is the leaf page.
leaf_page->Insert(key, value, comparator_);
buffer_pool_manager_->UnpinPage(root_page_id_, true); // unpin
}
2. insert
函數
這里的insert
函數可以直接用之前的KeyIndex
函數
INDEX_TEMPLATE_ARGUMENTS
int B_PLUS_TREE_LEAF_PAGE_TYPE::Insert(const KeyType &key, const ValueType &value, const KeyComparator &comparator) {
// 1. 邊界判斷
if (GetSize() == GetMaxSize()) {
std::cout << "leaf_page Insert: size=" << GetSize() << ", max_size=" << GetMaxSize() << std::endl;
}
// 2. find position
int pos = KeyIndex(key, comparator);
// 3. insert
// make room
for (int i = GetSize() - 1; i >= pos; i--) {
array_[i + 1] = array_[i];
}
array_[pos] = MappingType{key, value};
// 4. update size
IncreaseSize(1);
return GetSize();
}
2. 否則尋找到插入元素應該在的葉子結點,並插入(不分裂)
- 首先找到葉子結點
- 如果葉子結點內的元素個數小於最大值則直接插入
- 否則需要進行分裂。產生兩個新的結點。把元素上提
- 如果提到父親結點,父結點仍需要分裂。則遞歸進行分裂否則結束
如果葉子結點內的關鍵字小於m-1,則直接插入到葉子結點
1. LookUp函數實現
Lookup函數用來尋找包含輸入"key"的children pointer(其實就是page_id)
INDEX_TEMPLATE_ARGUMENTS
ValueType B_PLUS_TREE_INTERNAL_PAGE_TYPE::Lookup(const KeyType &key, const KeyComparator &comparator) const {
// 從第二個節點開始
for (int i = 1; i < GetSize(); i++) {
KeyType cur_key = array_[i].first;
if (comparator(key, cur_key) < 0) {
return array_[i - 1].second;
}
}
return array_[GetSize() - 1].second;
}
2. findLeafPage
由於要找到應該插入的LeafPage
所以這個函數狠狠重要。但是這里是非並發下插入,在這里用findLeafPage
進行對插入算法的測試。后面對於並發情況會有所修改。
- 從整個b+樹的根節點開始。一直向下找到葉子結點
- 因為b+樹是多路搜索樹,所以整個向下搜索就是通過key值進行比較
- 其中內部結點向下搜索的過程利用了上面提到的
lookup
函數
// only need for inserting test
INDEX_TEMPLATE_ARGUMENTS
Page *BPLUSTREE_TYPE::FindLeafPage(const KeyType &key, bool leftMost) {
throw Exception(ExceptionType::NOT_IMPLEMENTED, "Implement this for test");
if (root_page_id_ == INVALID_PAGE_ID) {
throw std::runtime_error("Unexpected. root_page_id is INVALID_PAGE_ID");
}
Page *page = buffer_pool_manager_->FetchPage(root_page_id_); // now root page is pin
BPlusTreePage *node = reinterpret_cast<BPlusTreePage *>(page);
while (!node->IsLeafPage()) {
InternalPage *internal_node = reinterpret_cast<InternalPage *>(node);
page_id_t next_page_id = leftMost ? internal_node->ValueAt(0) : internal_node->Lookup(key, comparator_);
Page *next_page = buffer_pool_manager_->FetchPage(next_page_id); // next_level_page pinned
BPlusTreePage *next_node = reinterpret_cast<BPlusTreePage *>(next_page);
buffer_pool_manager_->UnpinPage(node->GetPageId(), false); // curr_node unpinned
page = next_page;
node = next_node;
}
return page;
}
3. 無分裂直接插入
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::InsertIntoLeaf(const KeyType &key, const ValueType &value, Transaction *transaction) {
// 不考慮鎖的實現
{
if (IsEmpty()) {
StartNewTree(key, value);
return true;
}
//[Attention] 這里獲取到page是pined
Page *right_leaf = FindLeafPage(key, false);
LeafPage *leaf_page = reinterpret_cast<LeafPage *>(right_leaf);
// 1. if insert key is exist
if (leaf_page->Lookup(key, nullptr, comparator_)) {
buffer_pool_manager_->UnpinPage(leaf_page->GetPageId(), false); // unpined page
return false;
}
// 2. insert entry
leaf_page->Insert(key, value, comparator_);
// 下面分析需要分裂的情況
3. 分裂的情況
InsertLeaf主函數
接上文。
// 2.1 need to split
// split need to do two things
// 1. create new page copy [mid, r] to new page
// 2. if necessary 遞歸處理
if (leaf_page->GetSize() == maxSize(leaf_page) + 1) {
LeafPage *new_leaf = Split(leaf_page);
InsertIntoParent(leaf_page, new_leaf->KeyAt(0), new_leaf, transaction);
}
// 2.2 no need to split
// must unpin right leaf page
buffer_pool_manager_->UnpinPage(right_leaf->GetPageId(), true);
return true;
1. 調用split
函數對葉子結點進行分割
- split的時候會產生一個含有m-m/2個關鍵字的新結點。注意把兩個葉子結點連接起來。
- 這里注意split函數要區分葉子結點和內部結點。因為葉子結點需要更新雙向鏈表
INDEX_TEMPLATE_ARGUMENTS
template <typename N>
N *BPLUSTREE_TYPE::Split(N *node) {
// 1, ask a new page
page_id_t new_page_id;
Page *new_page = buffer_pool_manager_->NewPage(&new_page_id); // pinned
if (new_page == nullptr) {
throw std::string("out of memory");
}
N *new_node;
if (node->IsLeafPage()) {
LeafPage *leaf_node = reinterpret_cast<LeafPage *>(node);
LeafPage *new_leaf_node = reinterpret_cast<LeafPage *>(new_page);
new_leaf_node->Init(new_page_id, leaf_node->GetParentPageId(),leaf_max_size_);
leaf_node->MoveHalfTo(new_leaf_node);
// 葉子節點更新雙向鏈表
new_leaf_node->SetNextPageId(leaf_node->GetNextPageId());
leaf_node->SetNextPageId(new_leaf_node->GetPageId());
new_node = reinterpret_cast<N *>(new_leaf_node);
} else {
// 內部節點不需要設置雙向鏈表
InternalPage *internal_node = reinterpret_cast<InternalPage *>(node);
InternalPage *new_internal_node = reinterpret_cast<InternalPage *>(new_page);
new_internal_node->Init(new_page_id, internal_node->GetParentPageId(),internal_max_size_);
internal_node->MoveHalfTo(new_internal_node,buffer_pool_manager_);
new_node = reinterpret_cast<N *>(new_internal_node);
}
return new_node;
}
這里涉及到了MoveHalfTo
函數簡單的附上一下,這個非常簡單
INDEX_TEMPLATE_ARGUMENTS
void B_PLUS_TREE_LEAF_PAGE_TYPE::MoveHalfTo(BPlusTreeLeafPage *recipient) {
// 這里好像是說 你左邊多還是右邊多都行,書上是左邊多,我個人習慣右邊多
int moved_num = GetSize() - GetSize() / 2;
int start = GetSize() - moved_num;
CopyNFrom(array_ + start, moved_num);
IncreaseSize(-1 * moved_num);
recipient->IncreaseSize(moved_num);
}
2. InsertIntoParent
函數實現
這個函數的實現先看一下書上給出的算法
- 如果
old_node
就是根節點,那么就要創建一個新的節點R當作根節點。然后取key
的值當作根節點的值。修改old_node
和new_node
的父指針。以及根節點的孩子指針
// 1. old_node is root
if (old_node->IsRootPage()) {
page_id_t new_root_pid;
Page *page = buffer_pool_manager_->NewPage(&new_root_pid);
InternalPage *new_root_page = reinterpret_cast<InternalPage *>(page->GetData());
// new root page init
new_root_page->Init(new_root_pid, INVALID_PAGE_ID, internal_max_size_);
UpdateRootPageId(0); // update not insert
root_page_id_ = new_root_pid;
// set parent page id
new_root_page->PopulateNewRoot(old_node->GetPageId(), key, new_node->GetPageId());
old_node->SetParentPageId(new_root_pid);
new_node->SetParentPageId(new_root_pid);
buffer_pool_manager_->UnpinPage(new_root_pid, true);
}
- 找到分裂的葉子結點的父親節點隨后進行判斷
a. 如果可以直接插入則直接插入
b. 否則需要對父結點在進行分裂,即遞歸調用。
else {
page_id_t parent_pid = old_node->GetParentPageId();
Page *parent_page = buffer_pool_manager_->FetchPage(parent_pid); // parent_page pined
InternalPage *parent_node = reinterpret_cast<InternalPage *>(parent_page->GetData());
parent_node->InsertNodeAfter(old_node->GetPageId(), key, new_node->GetPageId());
if (parent_node->GetSize() == maxSize(parent_node) + 1) {
// need to split
InternalPage *new_parent_node = Split(parent_node); // new_parent_node pined
InsertIntoParent(parent_node, new_parent_node->KeyAt(0), new_parent_node);
buffer_pool_manager_->UnpinPage(new_parent_node->GetPageId(), true); // unpin new_parent_node
}
buffer_pool_manager_->UnpinPage(parent_node->GetPageId(), true); // unpin parent_node
}
好了第一部分的測試就通過了

附上一個pass的截圖完成第一部分✅
如果我們插入1、2、3、4、5那么我們用程序得到的結果如下
可以發現是完全正確的 🌟
附上通過的cmu網站滿分測試
3. ⚠️一些細節
1. 關於內部結點和葉子結點的區別
1.1 大小不一樣
內部結點的最大結點個數是比葉子結點多一
例如m = 3, 那么內部結點的個數就可以是3。而葉子結點則最多是2,但是內部結點的array[0]實際上就是個存地址的。它的key在我們的Draw結果圖中都不顯示。
1.2 在Split
的時候有區別
- 在葉子結點split的時候需要進行雙向鏈表的維護
- 而在內部結點則不需要
- 共有操作都是獲得一個新頁--> 類型轉換 ---> MoveHalfTo
2. upin的pin的注意事項
- 當你利用
FetchPage
拿到一個page的時候他就是pined - 當你使用完之后記得要
unpin
這很重要
3. debug的一些小技巧
- 利用可視化網站和代碼中給的
b_plus_print_test
這個測試,把輸入圖打印成xxx.dot
然后復制里面的內容在http://dreampuf.github.io/GraphvizOnline/顯示進行對比。 - 對於
Mac
系統利用Clion可以直接對測試文件debug。還是非常爽的。其中lldb
的利用非常重要。
4. maxSize的含義
這里要注意在進行B+樹初始化時候給的
internal_max_size
可以認為指的是指針數。也就是說假設我們有m = 3的b+樹
inter_max_size = 3 是可以有三個key。但是 leaf_max_size = 2 就只能包含一個key。這個在測試用例被卡才發現的。
所以maxSize()
函數可以這樣實現
INDEX_TEMPLATE_ARGUMENTS
template <typename N>
int BPLUSTREE_TYPE::maxSize(N *node) {
return node->IsLeafPage() ? leaf_max_size_ - 1 : internal_max_size_;
}