CMU15445 (Fall 2020) 数据库系统 Project#2 - B+ Tree 详解(下篇)
前言
上一篇博客中实现了单线程 B+ 树的查找、插入、删除和迭代操作,这篇博客将完成实验二的剩余任务:并发 B+ 树。实现 B+ 树并发访问最简单的方法就是在整棵树上加一把大锁,但是这样会导致过多线程处于阻塞状态,严重降低 B+ 树的性能。这篇博客将使用蟹行协议(crabbing protocol)实现并发。
蟹行协议
该协议的名字来源于螃蟹走路的方式:先移动一边的腿,然后另一边的,如此交替进行。该协议的加锁过程,从上往下和从下往上(发生分裂、合并或重新分布的情况),就像螃蟹移动一样。
查找
当查找一个键时,蟹行协议首先用共享模式锁住根结点。沿树向下遍历,在子结点上获得锁以后,它释放父结点上的锁。它重复该过程直至叶结点。

实际上在对根节点上锁之前,还需要对根节点的 id 进行上锁,防止根节点发生变化。所以需要在 BPlusTree 中添加一个 std::mutex root_latch_ 成员,在查找、插入、删除和迭代之前都需要获取 root_latch_。
插入和删除
插入和删除都需要对节点加写锁,由于插入可能导致叶节点分裂,删除可能导致叶节点的合并或者重新分配,所以在释放父节点的锁之前需要判断子节点是否安全。只有安全时才能释放所有祖先节点的锁,否则需要一直加锁下去。
插入时只要节点的键值对数量小于 max_internal_size_ - 1 (最后一个键值对充当哨兵),就不会分裂,就是安全的。
删除时需要特殊处理根节点:
- 根节点为叶节点,最小的键值对数量是 1,所以删除之前需要键值对数量大于 1 才是安全的;
- 根节点为叶节点,最小的键值对数量是 2,所以删除之前需要键值对数量大于 2 才是安全的;
如果节点不是根节点,需要删除后仍处于半满状态才是安全的。
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::IsPageSafe(BPlusTreePage *page, OperationType operation) {
auto size = page->GetSize();
switch (operation) {
case OperationType::READ:
return true;
case OperationType::INSERT:
return size < page->GetMaxSize() - 1;
case OperationType::REMOVE:
if (page->IsRootPage()) {
return page->IsLeafPage() ? size > 1 : size > 2;
}
return size > page->GetMinSize();
default:
break;
}
return false;
}
以删除为例,由于下图的 B 节点删除后不满足半满状态,所以不安全,无法释放 A 上的锁。

当走到 D 节点时,发现 D 是安全的,这时候可以释放所有祖先节点(A 和 B)上的锁。

对子节点的加锁发生在 FindLeafPage() 函数中,当子节点不安全时,调用 Transaction::AddIntoPageSet(Page *) 记录父节点。对于 root_latch_,当根节点不安全时,加到 transaction 里面的是空指针:
INDEX_TEMPLATE_ARGUMENTS
Page *BPLUSTREE_TYPE::FindLeafPage(const KeyType &key, bool leftMost, OperationType operation,
Transaction *transaction) {
if (operation == OperationType::READ) {
root_latch_.lock();
}
auto page_id = root_page_id_;
auto page = buffer_pool_manager_->FetchPage(page_id);
auto node = ToTreePage(page);
// 给根节点上锁
if (operation == OperationType::READ) {
page->RLatch();
root_latch_.unlock();
} else {
page->WLatch();
if (!IsPageSafe(node, operation)) {
transaction->AddIntoPageSet(nullptr); // 加一个空指针表示根节点 id 的锁
} else {
root_latch_.unlock();
}
}
// 定位到包含 key 的叶节点
while (!node->IsLeafPage()) {
InternalPage *inode = ToInternalPage(node);
// 寻找下一个包含 key 的节点
if (!leftMost) {
page_id = inode->Lookup(key, comparator_);
} else {
page_id = inode->ValueAt(0);
}
// 移动到子节点
auto child_page = buffer_pool_manager_->FetchPage(page_id);
// 给子节点上锁
if (operation == OperationType::READ) {
child_page->RLatch();
page->RUnlatch();
buffer_pool_manager_->UnpinPage(page->GetPageId(), false);
} else {
child_page->WLatch();
transaction->AddIntoPageSet(page);
// 如果子节点安全,就释放所有祖先节点上的写锁
if (IsPageSafe(ToTreePage(child_page), operation)) {
UnlockAncestors(transaction);
}
}
page = child_page;
node = ToTreePage(page);
}
return page;
}
如果子节点按钮,调用 UnlockAncestors() 来释放祖先节点上的锁,注意这里是先解锁再 Unpin(),如果先 Unpin() 可能导致在解锁之前页被换出,这时候解锁的是别人的页了:
INDEX_TEMPLATE_ARGUMENTS
void BPLUSTREE_TYPE::UnlockAncestors(Transaction *transaction, bool unpin) {
auto pages = transaction->GetPageSet().get();
while (!pages->empty()) {
auto page = pages->front();
pages->pop_front();
if (!page) {
root_latch_.unlock();
} else {
page->WUnlatch();
if (unpin) {
buffer_pool_manager_->UnpinPage(page->GetPageId(), false);
}
}
}
}
查找
GetValue() 函数修改如下,在判断树是否为空之前需要加锁:
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::GetValue(const KeyType &key, std::vector<ValueType> *result, Transaction *transaction) {
root_latch_.lock();
if (IsEmpty()) {
root_latch_.unlock();
return false;
}
root_latch_.unlock();
// 在叶节点中寻找 key
auto leaf_page = FindLeafPage(key);
LeafPage *leaf = ToLeafPage(leaf_page);
ValueType value;
auto success = leaf->Lookup(key, &value, comparator_);
if (success) {
result->push_back(value);
}
leaf_page->RUnlatch();
buffer_pool_manager_->UnpinPage(leaf->GetPageId(), false);
return success;
}
插入
在插入之前需要对 root_latch_ 上锁,在结束之前需要释放所有祖先节点上的锁:
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::Insert(const KeyType &key, const ValueType &value, Transaction *transaction) {
root_latch_.lock();
// 省略部分代码
}
/* Insert constant key & value pair into an empty tree */
INDEX_TEMPLATE_ARGUMENTS void BPLUSTREE_TYPE::StartNewTree(const KeyType &key, const ValueType &value) {
// 创建一个叶节点作为根节点,并插入新数据
// 省略部分代码
UpdateRootPageId(1);
root_latch_.unlock();
buffer_pool_manager_->UnpinPage(root_page_id_, true);
}
/* Insert constant key & value pair into leaf page */
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::InsertIntoLeaf(const KeyType &key, const ValueType &value, Transaction *transaction) {
// 定位到包含 key 的叶节点
auto leaf_page = FindLeafPage(key, false, OperationType::INSERT, transaction);
LeafPage *leaf = ToLeafPage(leaf_page);
// 不能插入相同的键
ValueType exist_value;
if (leaf->Lookup(key, &exist_value, comparator_)) {
UnlockAncestors(transaction);
leaf_page->WUnlatch();
buffer_pool_manager_->UnpinPage(leaf->GetPageId(), false);
return false;
}
// 省略部分代码
UnlockAncestors(transaction);
leaf_page->WUnlatch();
buffer_pool_manager_->UnpinPage(leaf->GetPageId(), true);
return true;
}
/* Insert key & value pair into internal page after split */
INDEX_TEMPLATE_ARGUMENTS
void BPLUSTREE_TYPE::InsertIntoParent(BPlusTreePage *old_node, const KeyType &key, BPlusTreePage *new_node,
Transaction *transaction) {
// 根节点发生分裂需要新建一个根节点,B+树的高度 +1
if (old_node->IsRootPage()) {
// 省略部分代码
UpdateRootPageId(0);
buffer_pool_manager_->UnpinPage(root_page_id_, true);
UnlockAncestors(transaction, false);
return;
}
// 省略部分代码
// 父节点溢出时需要再次分裂
if (size == internal_max_size_) {
// 省略
} else {
UnlockAncestors(transaction, false);
buffer_pool_manager_->UnpinPage(parent_id, true);
}
}
删除
删除和插入类似,唯一需要注意的是对兄弟节点进行加锁,防止迭代的时候被访问,调整结束后立即释放兄弟节点上的锁:
INDEX_TEMPLATE_ARGUMENTS
void BPLUSTREE_TYPE::Remove(const KeyType &key, Transaction *transaction) {
root_latch_.lock();
if (IsEmpty()) {
root_latch_.unlock();
return;
}
// 定位到叶节点并删除键值对
auto leaf_page = FindLeafPage(key, false, OperationType::REMOVE, transaction);
LeafPage *leaf = ToLeafPage(leaf_page);
auto old_size = leaf->GetSize();
auto size = leaf->RemoveAndDeleteRecord(key, comparator_);
// 叶节点删除之后没有处于半满状态需要合并相邻节点或者重新分配
if (size < leaf->GetMinSize() && CoalesceOrRedistribute(leaf, transaction)) {
transaction->AddIntoDeletedPageSet(leaf->GetPageId());
}
UnlockAncestors(transaction);
leaf_page->WUnlatch();
buffer_pool_manager_->UnpinPage(leaf->GetPageId(), old_size != size);
// 不知道为什么删除之后会导致堆溢出错误
// DeletePages(transaction);
}
INDEX_TEMPLATE_ARGUMENTS
template <typename N>
bool BPLUSTREE_TYPE::CoalesceOrRedistribute(N *node, Transaction *transaction) {
// 找到相邻的兄弟节点并加锁,省略部分代码
auto sibling_page = buffer_pool_manager_->FetchPage(parent->ValueAt(sibling_index));
N *sibling = reinterpret_cast<N *>(sibling_page->GetData());
sibling_page->WLatch();
// 如果两个节点的大小和大于 max_size-1,就直接重新分配,否则直接合并兄弟节点
bool is_merge = sibling->GetSize() + node->GetSize() <= node->GetMaxSize() - 1;
if (is_merge) {
Coalesce(&sibling, &node, &parent, index, transaction);
} else {
Redistribute(sibling, node, index);
}
// 兄弟节点解锁
sibling_page->WUnlatch();
buffer_pool_manager_->UnpinPage(parent->GetPageId(), true);
buffer_pool_manager_->UnpinPage(sibling->GetPageId(), true);
return is_merge;
}
迭代
迭代时需要对叶节点加读锁,析构迭代器时需要释放锁:
INDEX_TEMPLATE_ARGUMENTS
INDEXITERATOR_TYPE &INDEXITERATOR_TYPE::operator++() {
if (isEnd()) {
return *this;
}
LeafPage *leaf = reinterpret_cast<LeafPage *>(page_);
if (index_ < leaf->GetSize() - 1) {
index_++;
} else {
Page* old_page = page_;
// 移动到下一页
page_id_ = leaf->GetNextPageId();
if (page_id_ != INVALID_PAGE_ID) {
page_ = buffer_pool_manager_->FetchPage(page_id_);
page_->RLatch();
} else {
page_ = nullptr;
}
index_ = 0;
old_page->RUnlatch();
buffer_pool_manager_->UnpinPage(old_page->GetPageId(), false);
}
return *this;
}
INDEX_TEMPLATE_ARGUMENTS
INDEXITERATOR_TYPE::~IndexIterator() {
if (!isEnd()) {
page_->RUnlatch();
buffer_pool_manager_->UnpinPage(page_->GetPageId(), false);
}
};
测试
在终端输入下述指令完成编译:
cd build
cmake ..
make
# 从 Grade scope 拔下来的测试用例
make b_plus_tree_checkpoint_2_concurrent_test
make b_plus_tree_bench_test
./test/b_plus_tree_checkpoint_2_concurrent_test
./test/b_plus_tree_bench_test
测试结果如下,只给虚拟机分配了一个核,速度可能慢了一些:

后记
纸上得来终觉浅,绝知此事要躬行。历经三天终于完成了 B+ 树的实验,通过这次实验可以加深对索引结构的理解,可以说是非常硬核的一次实验了。刚开始有点无从下手,因为要完成的函数实在太多了。写着写着发现可以自顶而下,先完成 BPlusTree 方法上的逻辑,再深入到底层的 BPlusTreePage 实现对应的方法,似乎也没那么难以下手了。完成之后可以明显感受到精神力得以增强,信心开始膨胀(不是。
借用屑老板的话:这是一场「试炼」,我认为这就是一场为了战胜过去的「试炼」,只有在战胜了那些幼稚的过去,人才能有所成长。嗯?你也是那样的吧?
CMU15445 (Fall 2020) 数据库系统 Project#2 - B+ Tree 详解(下篇)的更多相关文章
- CMU15445 (Fall 2019) 之 Project#1 - Buffer Pool 详解
前言 这个实验有两个任务:时钟替换算法和缓冲池管理器,分别对应 ClockReplacer 和 BufferPoolManager 类,BufferPoolManager 会用 ClockReplac ...
- CMU15445 (Fall 2019) 之 Project#4 - Logging & Recovery 详解
前言 这是 Fall 2019 的最后一个实验,要求我们实现预写式日志.系统恢复和存档点功能,这三个功能分别对应三个类 LogManager.LogRecovery 和 CheckpointManag ...
- CMU15445 (Fall 2019) 之 Project#2 - Hash Table 详解
前言 该实验要求实现一个基于线性探测法的哈希表,但是与直接放在内存中的哈希表不同的是,该实验假设哈希表非常大,无法整个放入内存中,因此需要将哈希表进行分割,将多个键值对放在一个 Page 中,然后搭配 ...
- CMU15445 (Fall 2019) 之 Project#3 - Query Execution 详解
前言 经过前面两个实验的铺垫,终于到了给数据库系统添加执行查询计划功能的时候了.给定一条 SQL 语句,我们可以将其中的操作符组织为一棵树,树中的每一个父节点都能从子节点获取 tuple 并处理成操作 ...
- iOS学习——iOS项目Project 和 Targets配置详解
最近开始学习完整iOS项目的开发流程和思路,在实际的项目开发过程中,我们通常需要对项目代码和资料进行版本控制和管理,一般比较常用的SVN或者Github进行代码版本控制和项目管理.我们iOS项目的开发 ...
- CMU15445 之 Project#0 - C++ Primer 详解
前言 这个实验主要用来测试大家对现代 C++ 的掌握程度,实验要求如下: 简单翻译一下上述要求,就是我们需要实现定义在 src/include/primer/p0_starter.h 中的三个类 Ma ...
- [Project] SpellCorrect源码详解
该Project原来的应用场景是对电商网站中输入一个错误的商品名称进行智能纠错,比如iphoae纠错为iphone.以下介绍的这个版本对其作了简化,项目源代码地址参见我的github:https:// ...
- Project Server 2010 配置详解
应公司要求,需要加强对项目的管理.安排我学习一下微软的Project是如何进行项目管理的,并且在公司服务器上搭建出这样的一个项目管理工具.可以通过浏览器就可以访问.我因为用的单机是Project Pr ...
- 二叉查找树(binary search tree)详解
二叉查找树(Binary Search Tree),也称二叉排序树(binary sorted tree),是指一棵空树或者具有下列性质的二叉树: 若任意节点的左子树不空,则左子树上所有结点的值均小于 ...
- BTree和B+Tree详解
https://www.cnblogs.com/vianzhang/p/7922426.html B+树索引是B+树在数据库中的一种实现,是最常见也是数据库中使用最为频繁的一种索引.B+树中的B代表平 ...
随机推荐
- VBA GET POST HTTP VBA网络爬虫 最新Excel自动获取股票信息源码 EXCEL自动获取网络数据 最新VBA自动抓取股票数据源码
最新Excel自动获取股票信息源码 EXCEL自动获取网络数据 最新VBA自动抓取股票数据源码 通过接口获取股票数据内容的主要优点包括以下几点: 实时性高:通过访问股票数据接口,可以实时获取到股票的实 ...
- R语言文本数据挖掘(四)
文本分词,就是对文本进行合理的分割,从而可以比较快捷地获取关键信息.例如,电商平台要想了解更多消费者的心声,就需要对消费者的文本评论数据进行内在信息的数据挖掘分析,而文本分词是文本挖掘的重要步骤.R语 ...
- PyCharm解决Git冲突
技术背景 在前面的一篇博客中,我们介绍了Fork到自己名下的本地仓库如何与远程原始仓库创建链接的方法.在这篇文章中,我们将要讲解如何应对在这种异步开发的过程中经常有可能会遇到的Git冲突问题,在Pyc ...
- pandas之缺失值处理
在一些数据分析业务中,数据缺失是我们经常遇见的问题,缺失值会导致数据质量的下降,从而影响模型预测的准确性,这对于机器学习和数据挖掘影响尤为严重.因此妥善的处理缺失值能够使模型预测更为准确和有效. 为什 ...
- PySimpleGU之运行多个窗口
这是PySimpleGUI继续简单的地方,但是问题空间刚刚进入了"复杂"领域. 如果您希望在事件循环中运行多个窗口,则有两种方法可以执行此操作. 当第二个窗口可见时,第一个窗口不会 ...
- [NotePad++]NotePad++实用技巧
2 应用技巧 2.1 匹配并捕获/截取 截取第1列的数据 截取前 "(.*)", "(.*)", "(.*)"\)\); 截取后: 2.2 ...
- 用Abp实现双因素认证(Two-Factor Authentication, 2FA)登录(一):认证模块
@ 目录 原理 用户验证码校验模块 双因素认证模块 改写登录 在之前的博文 用Abp实现短信验证码免密登录(一):短信校验模块 一文中,我们实现了用户验证码校验模块,今天来拓展这个模块,使Abp用户系 ...
- python从shp文件中读取经纬度数据
python从shp文件中读取经纬度数据 没有接触过GIS的人来说shp文件很陌生而且很难打开查看,好在python可以从中提取出自己想要的数据 pyshp库的安装 python的pyshp库可以实现 ...
- Redis 数据类型 Stream
Redis 数据类型 Stream Redis 常用命令,思维导图 >>> Redis Stream 是 Redis 5.0 版本新增加的数据结构. Redis Stream 主要用 ...
- PHP创建SqlLite数据表并让ID自增
<?php class MyDB extends SQLite3 { function __construct() { $this->open('test.db'); } } $db = ...