文章目录
-
- Task1 B+树页
-
- B+树页
- B+树内部结点
- B+树叶子结点
- Task2 B+树操作
-
- Task2 B+树插入和搜索的单一值
-
- 插入服务器托管网单一值
- 搜索单一值
- Task2 B+树删除
- Task3 叶子扫描的迭代器
- Task4 并行索引
Task1 B+树页
B+树页
实际上是每个B+树页面的标题部分,包含叶子页面和内部页面共享的信息。
报头格式(大小以字节为单位,共12字节):
| PageType (4) | CurrentSize (4) | MaxSize (4) |
// define page type enum
enum class IndexPageType { INVALID_INDEX_PAGE = 0, LEAF_PAGE, INTERNAL_PAGE };
// 成员变量,内部页和叶页共享的属性
IndexPageType page_type_ __attribute__((__unused__));
int size_ __attribute__((__unused__)); // 键值对的数量,内部页的第一个键为无效值
int max_size_ __attribute__((__unused__)); // 键值对的容量
B+树内部结点
在内部页面中存储n个索引键和n+1个子指针(page_id)。
指针PAGE_ID(i)指向一个子树,其中所有键K满足:K(i)
注意:由于键的数量不等于子指针的数量,所以第一个键总是无效的。也就是说,任何搜索/查找都应该忽略第一个键。
内部页面格式(键按递增顺序存储):
| HEADER | KEY(1)+PAGE_ID(1) | KEY(2)+PAGE_ID(2) | ... | KEY(n)+PAGE_ID(n) |
#define MappingType std::pairKeyType, ValueType>
//key -> page_id 的映射
MappingType array_[0];
B+树叶子结点
只支持唯一键。
叶页格式(键按顺序存储):
| HEADER | KEY(1) + RID(1) | KEY(2) + RID(2) | ... | KEY(n) + RID(n)
HEADER格式(大小为字节,共16字节):
| PageType (4) | CurrentSize (4) | MaxSize (4) | NextPageId (4)
page_id_t next_page_id_; // 先定位到一个叶子,再顺序向后扫描
//key->record_id 的映射,record id = page id combined with slot id
MappingType array_[0];
Task2 B+树操作
大部分操作在b_plus_tree.cpp
实现
Context
类用于记录操作B+树过程中需要记忆的路径、读写保护等
class Context {
public:
// 当您插入/删除B+树时,将头页的写保护存储在这里。
// 当您想要解锁所有内容时,请记住取消标题页保护并将其设置为空值,即header_page.reset()
std::optionalWritePageGuard> header_page_{std::nullopt};
//在这里保存根页面id,以便更容易地知道当前页面是否是根页面。
page_id_t root_page_id_{INVALID_PAGE_ID};
// 要修改的页面的写保护的队列。
std::dequeWritePageGuard> write_set_;
// 读保护队列
std::dequeReadPageGuard> read_set_;
auto IsRootPage(page_id_t page_id) -> bool { return page_id == root_page_id_; }
};
Task2 B+树插入和搜索的单一值
插入单一值
BPLUSTREE_TYPE::Insert()
实现,若BPLUSTREE_TYPE::header_page_id_
属性标识的根节点的页(BPlusTreeHeaderPage
)的根节点页id无效,说明没有根页面,调用缓冲区的NewPage()
新建root页,然后强转为BPlusTreeLeafPage
类型,此时根节点就是叶子节点,然后BPlusTreeLeafPage::InsertAtLast()
根叶后插k-v,标识正在使用该页,清空header_page_
(检查header_page_id_
标识的根节点页时,页就存在这里)。若有根节点就获取根节点页id,调用InsertIntoLeaf()
BPLUSTREE_TYPE::InsertIntoLeaf()
开头就调用了BPLUSTREE_TYPE::FindLeaf()
,此函数的作用是为插入/删除结点操作寻找目标叶,结果存在B+树类的Context
类成员的write_set_
中。回到函数,取出查找到的页,强转为BPlusTreeLeafPage
后直接调用BPlusTreeLeafPage::Insert()
插入k-v,若插入失败说明是重复键值对,清空header_page_
和write_set_
后退出,并且若叶页大小没超过叶子最大容量则此插入是安全插入,清空header_page_
和write_set_
后退出,剩下的情况就是叶页满了,需要分裂,调用Split()
进行分裂,返回的新页引用是满页分裂出的兄弟叶页,之后调用InsertIntoParent()
修改父结点结构。
BPLUSTREE_TYPE::InsertIntoParent()
是个直接递归,在函数开头有个判断write_set_
内的锁有一个时,这些锁是在InsertIntoLeaf()
中调用FindLeaf()
时产生的,表示修改该叶子会导致write_set_
中的这些结点页被修改,即最接近叶子的安全结点到叶子节点这一路径上的所有结点的锁。在write_set_
中只剩一个页时,NewPage()
新建一个页,当作新根节点,然后旧根节点和分裂出的新结点做新根的子节点,返回。通过write_set_
获取父结点,然后调用BPlusTreeInternalPage::InsertAfterValue()
将分裂后产生的新节点插入父节点,write_set_
弹出旧结点(原来不安全的结点)。当父节点的大小超过了最大容量,则父节点不安全,调用Split()
分裂并调用InsertIntoParent()
递归,若是安全结点,则清理header_page_
和write_set_
,结束。
BPLUSTREE_TYPE::Split()
分裂结点,返回新的兄弟页。NewPage()
创建新页,叶子节点分裂就将新页和待分裂的页强转为叶页,初始化新叶页后旧叶页调用BPlusTreeLeafPage::SplitLeafTo()
,取出旧叶页的后一半数据给新叶页,并且设置旧叶页和新叶页的next_page_id
。最后设置正在使用新叶页,然后返回。若分裂内部节点,将新页和待分裂的页强转为内部结点页,新结点页初始化后旧结点页调用BPlusTreeInternalPage::SplitInternalTo()
取内部节点的最大容量加一的一半(后半段)的内容给新内部节点,限定结点大小。设置新结点页正在使用后返回。
这里我不理解的是在InsertIntoParent()
中仅仅用if (ctx.write_set_.size() == 1)
就确定是根节点分裂
搜索单一值
BPLUSTREE_TYPE::GetValue()
实现,获取头部页的写锁header_page_
,通过它获得根节点id,将根节点读锁加入read_set_
然后强转为BPlusTreePage
,清空header_page_
,强转结点为BPlusTreeInternalPage
,调用BPlusTreeInternalPage::FindInternelKey()
按key二分查找中间节点的子节点引用,查到的页加读锁,加入read_set_
然后将原先在read_set_
中的父页弹出,这样就实现了读锁的”下沉”,之后进入循环直到页是叶页。循环出来后将页强转为BPlusTreeLeafPage
类,之后调用BPlusTreeLeafPage::FindKey()
二分查找叶子节点的值,找到后组装传参vector。
Task2 B+树删除
BPLUSTREE_TYPE::Remove()
实现,获取头部页写锁并得到根节点id,调用FindLeaf()
查找key,获取查到的页强转成BPlusTreeLeafPage
,调用BPlusTreeLeafPage::Del服务器托管网eteKey()
先二分查找叶子结点key,若没有调用DeleteKeyAt()
删除数组中对应的k-v,删除失败就清理header_page_
和write_set_
并退出,若删除的k-v所在的叶页大小大于等于最小容量(叶子结点是max_size_/2
,内部节点是(max_size_+1)/2
)时,即为安全删除,清空write_set_
并退出,若根节点时叶子,叶子根节点有一个键值对就能退出。最后都没有退出的就是需要非安全删除情况,可能会有修改父节点key、小于最小尺寸需从兄弟节点处拿甚至兄弟节点也不足直接合并,调用DealNode()
合并或删除叶页。
BPLUSTREE_TYPE::DealNode()
是个间接递归,它调用Merge()
而Merge()
调用它。处理借取键值对或合并节点,write_set_
的最后一个值是这个函数体需要处理的不安全的节点(已进行删除工作)。
-
若是根节点
- 根叶:清理
write_set_
和header_page_
- 不是叶:强转为内部节点,取第0个的引用做根节点id(
header_page_
强转为BPlusTreeHeaderPage
可取root_page_id_
)
返回
- 根叶:清理
-
不是根
从
write_set_
取出该页的父页,强转为内部节点-
是叶
强转为叶页,父页调用FindInternelKey()
二分查找叶子key获得index左边有值,有左兄弟(index > 0):取左兄弟(父页的index-1)的写锁存入
write_set_
,强转为叶页后保存变量右边有值,有右兄弟(index write_set_,强转为叶页后保存变量
- 左右兄弟都在(左叶页变量 && 右叶页变量):
- 两个兄弟都不足(兄弟大小 Merge()与右节点合并,返回
- 右边有剩余(右兄弟大小 > 最小容量):清空
header_page_
,向兄弟节点借键值对表示父页是安全节点,释放write_set_
中父页的祖先节点及根的锁,叶页调用LendFromBrother()
向右兄弟借,之后父页调用SetKeyAt()
更新父页中的右兄弟结点在父页的索引,然后清理write_set_
- 左边有剩余(else):清空
header_page_
,释放write_set_
中父页的祖先节点及根的锁,叶页调用LendFromBrother()
向左兄弟借,父页调用SetKeyAt()
更新父页中的右兄弟结点在父页的索引,然后清理write_set_
- 只有左兄弟(左叶页变量):
- 左兄弟不足(左兄弟大小 Merge()与左兄弟合并,返回
- 左边有剩余:清空
header_set_
,释放write_set_
中父页祖先节点及根的锁,叶页调用LendFromBrother()
向左兄弟借,父页调用SetKeyAt()
更新父页中的左兄弟结点在父页的索引,然后清理write_set_
- 只有右兄弟(右叶页变量):
- 右兄弟不足(右兄弟大小 Merge()与右兄弟合并,返回
- 右边有剩余:清空
header_set_
,释放write_set_
中父页祖先节点及根的锁,叶页调用LendFromBrother()
向右兄弟借,父页调用SetKeyAt()
更新父页中的右兄弟结点在父页的索引,然后清理write_set_
- 左右兄弟都在(左叶页变量 && 右叶页变量):
-
是内部结点
强转为内部结点页,父页调用
FindInternelKey()
二分查找内部结点获得index左边有值,有左兄弟(index > 0):取左兄弟(父页的index-1)的写锁存入
write_set_
,强转为内部结点页后保存变量右边有值,有右兄弟(index write_set_,强转为内部结点后保存变量
- 左右兄弟都在(左结点页变量 && 右结点页变量):
- 两个兄弟都不足(兄弟大小 Merge()与右节点合并,返回
- 右边有剩余(右兄弟大小 > 最小容量):清空
header_page_
,释放write_set_
中父页的祖先节点及根的锁,内部结点页调用LendFromBrother()
向右兄弟借,之后父页调用SetKeyAt()
更新父页中的右兄弟结点在父页的索引,然后清理write_set_
- 左边有剩余(else):清空
header_page_
,释放write_set_
中父页的祖先节点及根的锁,叶页调用LendFromBrother()
向左兄弟借,父页调用SetKeyAt()
更新父页中的右兄弟结点在父页的索引,然后清理write_set_
- 只有左兄弟(左结点页变量):
- 左兄弟不足(左兄弟大小 Merge()与左兄弟合并,返回
- 左边有剩余:清空
header_set_
,释放write_set_
中父页祖先节点及根的锁,叶页调用LendFromBrother()
向左兄弟借,父页调用SetKeyAt()
更新父页中的左兄弟结点在父页的索引,然后清理write_set_
- 只有右兄弟(右结点页变量):
- 右兄弟不足(右兄弟大小 Merge()与右兄弟合并,返回
- 右边有剩余:清空
header_set_
,释放write_set_
中父页祖先节点及根的锁,叶页调用LendFromBrother()
向右兄弟借,父页调用SetKeyAt()
更新父页中的右兄弟结点在父页的索引,然后清理write_set_
- 左右兄弟都在(左结点页变量 && 右结点页变量):
-
BPLUSTREE_TYPE::Merge()
总是把右边节点的键值对转移给左边,再删除右边节点,这样方便更新NextPageId
,若传入的经删除操作的页是叶子节点,将该页和兄弟页强转为叶页后,判断是右兄弟,兄弟结点就调用BPlusTreeLeafPage::MoveAllTo()
将自己的k-v顺序插入该页的末尾,将该页的NextPageId
设置为右兄弟的,设置删除结点索引为右兄弟索引(该页索引+1)。若判断是左兄弟,该页调用BPlusTreeLeafPage::MoveAllTo()
将自己的k-v顺序插入左兄弟的尾部,左兄弟的NextPageId
设置为该页的,设置删除节点索引为该页索引。若进行删除操作的页是内部结点,强转该页和兄弟页为内部节点页,判断是右兄弟就把兄弟k-v插入该页末尾,设置删除索引为兄弟,反之同理。之后将write_set_
中该页父页的孩子出栈解锁,只剩父页和其祖先节点,把父页强转为内部节点页,调用DeleteKeyAt()
将删除结点索引指定的结点页删除。若父页大小大于等于最小容量(安全删除),清理write_set_
后就可返回,若是根结点,内部根节点右两个键值对(容量大于1)就行,返回,其他情况就是不安全删除,调用DealNode()
进行间接递归。
BPlusTreeLeafPage::LendFromBrother()
从brother移动一个键值对过来,兄弟调用KeyAt()
和ValueAt()
,针对传入是否是左兄弟,将k-v插入结点的头或尾并设置兄弟的大小或删除k-v,最后返回新key。
Task3 叶子扫描的迭代器
必须添加一个c++迭代器,以有效地支持对叶页中的数据进行有序扫描。基本思想是存储兄弟指针,这样就可以有效地遍历叶页,然后实现一个迭代器,按顺序遍历每个叶页中的每个键值对。
Begin()
和End()
函数在b_plus_true.cpp
中,而迭代器类在index_iterator.cpp
中实现
//索引迭代器类的私有属性
BPlusTreeLeafPageKeyType, ValueType, KeyComparator> *leaf_page_{nullptr}; // 此迭代器指向的叶子
int index_{-1}; // 表示此迭代器现在指向leaf_page_中某叶页的第几个键值对
page_id_t my_page_id_;//表示迭代器指向的leaf_page_中某叶页的页id
BufferPoolManager *bpm_{nullptr};
重载了* ++ == !=
四个符号来对索引迭代器进行操作
BPLUSTREE_TYPE::Begin()
有重载,分别对应处理没给key和给了key的情况。没给就直接调用FindLeafPage()
传入空key并指定找最左边的k-v,这样就返回了叶页链表首页,然后强转为叶页,构造索引迭代器后返回。传入key的Begin()
多加了一层检测传回的页是否真的有key,没有说明FindLeafPage()
没有找到,返回空索引迭代器。
BPLUSTREE_TYPE::End()
同无参Begin()
只不过指定找的是最右边的k-v(即index=internal_page->GetSize()-1
)
BPLUSTREE_TYPE::FindLeafPage()
无锁获取根节点,根据传入的indextype
判断需要的是最左/右的页还是进行k-v查找。
Task4 并行索引
见Task 2
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net