2.1 Insertion
调用
guard_tmp.AsMut<BPlusTreeLeafPage>()
Candidate template ignored: invalid explicitly-specified argument for template parameter 'T'
这是因为BPlusTreeLeafPage也为模板类,需要知道模板参数。 BPlusTree已经替我们声明过了,使用LeafPage即可。
(1) 这里按照书中伪代码来写即可,在进行split时,书中伪代码首先将原先节点读入内存(作为局部变量),进行插入后,再进行分配。此处可以做一个优化,我们可以知道新key插入的索引位置new_idx
,再根据new_idx
和划分索引的关系进行判断复制起始位置,省去读入内存这一步,直接进行两页直接的复制即可。
(2)另一点需要注意的是,书中划分叶子节点是给的是
⌈
n
/
2
⌉
\lceil n/2 \rceil
⌈n/2⌉,这是因为一共有
n
n
n个key(算上要插入的key),代码实现中应该修正为
⌈
(
n
+
1
)
/
2
⌉
\lceil (n+1)/2 \rceil
⌈(n+1)/2⌉。其中
n
n
n为叶子节点的最大数量。
不按照书中策略来也可行,但一般情况下会导致树的高度比这种策略要高,从而造成性能损失。,假设我就强行指定原节点就保留一个key,剩下的都给新节点,只要向上层插入的key是正确的就可以保证有正确的行为。
- bug记录
在ScaleTest测试中出现如下BUG。
正常情况下加锁后,此处会出现一个Writer ID(线程PID)
从图中也可以看出,中途是没有加读锁的。此bug的诡异之处是在加锁前明明没有Writer ID,但提示出现死锁错误。
目前的解决措施是:将FetchPageWrite都改为FetchPageBasic,write_set_也改为BasicPageGuard的双端队列
(1)原因更新:被这个BUG折磨了一天也没有找到原因。逻辑上感觉没问题。在按照上述解决措施强行往下写后,反过来再改为FetchPageWrite时,偶然发现了原因。
当keys中元素很多时,就会出现此BUG
for (auto key : keys) {
int64_t value = key & 0xFFFFFFFF;
rid.Set(static_cast<int32_t>(key >> 32), value);
index_key.SetFromInteger(key);
tree.Insert(index_key, rid, transaction); // 这里都为FetchPageWrite
std::string str = "/home/hsfw/bustub-zhang/build/my-tree.dot";
tree.Draw(bpm, str); // 这里都为FetchPageBasic
}
2.2 Delete
这块代码量比较多,但实际上比较简单。
2.3 优化措施
-
将缓存管理改为大锁的形式,再结合乐观锁机制(乐观锁有所提升但效果并不明显),最终QPS如下图所示:最高能到14W左右。即使将叶子节点的MinSize改为1效果也没有提升(本地测试会提升很多)。
-
WARN - page not exist的错误就是P1中提到的细节。
-
使用单个读写锁的方式,但会导致锁竞争时间急剧上升(本地测试需要200s,加大锁的为十几秒)。因此采用了多个读写锁的方式,在project 1中也能获得2w多分,与简单的大锁方式有显著提升,但在P3中,还没有大锁得分高,比较奇怪。感觉加大锁和使用细粒度锁的方式区别不大。
知乎上有人推荐,在
UnpinPage
中实现写回磁盘的操作,也可以避免这种情况的发生,但感觉会造成多次的写入,因为UnpinPage
的调用比较频繁。 -
删除操作中,如果发生Redistribution时,也可以提前释放祖先节点以增加并行性(不包括当前父节点)。
-
避免在Internal中查找
page_id
,因为其使用的是线性搜索的策略,比较慢。在插入时需要知道新节点插入到父节点的哪个索引处,在删除时也需要找到’借用数据’的 N ′ N' N′,这些都需要知道当前节点在父节点的索引。可以在从根到叶子节点遍历时,保存下各步的索引,方便后续使用。 -
(优化细节)再将
PageGuard
转换为实际指针时,尽量多使用As,因为AsMut会设置脏页,弹出时可能会需要写入磁盘。之前在插入和删除的遍历操作中,我都使用了AsMut,现在统一改为As。 -
在实现乐观锁的过程中,因为叶子节点需要加写锁,中间节点需要读锁。因此提前得知树的高度,这样才能确定需要加哪种锁。
维护树高比较简单,根结点发生分裂或合并时才会改变树高,而只有持有header
写锁的情况下才会修改tree_height_
,此处需要注意,如果直接在遍历时直接使用tree_height_
就会造成问题。因为tree_height_
变量很有可能在另一个线程中发生了改变,如果依据tree_height_
遍历的话就会出现段错误,访问到无效内存。正确的方式是在持有header
读锁时,就先把tree_height_
变量保存为临时变量. -
插入时会重分配的思路