CMU15445Lab2BPlusTree
CMU15445Lab2BPlusTree
Hoshea Zhang整体结构
父页面的内部结构
- 内部结构固定24B
内部页面(索引页面)
除了有header,还有data字段
数据存储在KV键值对上,这里要小于4K大小
value都是指针 指向另一个页面
b+树的叶子页面
- 相比于非叶子页面,页面头多了一个字段
NextPageId
b+树的特点
树查找 getValue
核心流程
- 从根节点开始查找
- 内部节点中:使用二分查找,找到大于等于key的value指针PageId,通过PageId获取到新的节点,如果是内部节点则继续这个步骤,如果是叶子节点则执行对应的查找规则。
- 叶子节点中:使用二分查找,找到恰好等于key的value获取RID
节点分为 internal page 和 leaf page,每个 page 上的 key 有序排列。当拿到一个 key 需要查找对应的 value 时,首先需要经由 internal page 递归地向下查找,最终找到 key 所在的 leaf page。这个过程可以简化为一个函数 Findleaf()
。
Findleaf()
从 root page 开始查找。在查找到 leaf page 时直接返回,否则根据 key 在当前 internal page 中找到对应的 child page id,递归调用Findleaf
。根据 key 查找对应 child id 时,由于 key 是有序的,可以直接进行二分搜索。15-445 Lecture 中也介绍了一些其他的方法,比如用 SIMD 并行比较,插值法等等。在这里二分搜索就可以了。
internal page 中储存 key 和 child page id,那么在拿到 page id 后如何获得对应的 page 指针?用 Project 1 中实现的 buffer pool。
1 | Page *page = buffer_pool_manager_->FetchPage(page_id); |
找到 leaf page 后,同样是二分查找 key,找到对应的 record id。
要注意unpin操作
我们在拿到 page id 后,调用 buffer pool 的
FetchPage()
函数来获取对应的 page 指针。要注意的是,在使用完 page 之后,需要将 page unpin 掉,否则最终会导致 buffer pool 中的所有 page 都被 pin 住,无法从 disk 读取其他的 page。
树插入
主要流程
- 目标:插入key和value到B+树上
- 整体步骤
- 从根节点开始
- 如果是空树,新建一个根节点,直接插入到根节点。
- 如果不是空树,根据查询的流程找到key对应的叶子页面。
- 如果叶子,没有满(size < n - 1)则直接插入,结束。
- 如果叶子,满了。
- 将原来的数据和新插入的数据,平均分成两份,新建一个页面存放另一半数据。
- 将另一半中最小的key标记为K’,将这个K’插入到父节点中。
- 从根节点开始
- 插入父节点步骤
- 如果分裂的节点L是根节点,则创建一个新根,且只有1个key,就是传入的K’
- 如果分裂的节点L是普通节点,则插入K’和对应的PageId到当前节点中。
- 插入后内部节点容量没满:
- 结束。
- 插入后内部节点容量满了:
- 分裂成两个页面,新页面的最小值K’’插入到父节点,递归执行。
- 插入后内部节点容量没满:
说的比较简单,反正可以去网上看一下怎么搞
树删除
流程图
主要步骤
- 根据查找流程找到key对应的叶子页面。
- 从这个叶子页面删除对应的Key和Value。
- 如果删除的节点是根且只有一个孩子,则让它的孩子变成新的根,删除自身。
- 如果删除后页面中KV数量太少,则需要合并或者重分配。
- 直接合并。如果相邻节点能放得下两个节点的数据,则直接放。
- 需要区分叶子和非叶子
- 删除自身节点。
- 删除父节点指向自身的指针。
- 需要区分叶子和非叶子
- 重分配。如果放不下,则进行数据重分配。从左邻居的末尾获取一个元素放到自身的头部。
- 如果是叶子:直接放。更新父节点。
- 如果是非叶子。把父节点降下来再放。更新父节点。
- 直接合并。如果相邻节点能放得下两个节点的数据,则直接放。
索引迭代器
目的
为了高效地检索所有的叶子页,基本思路是组织成一个单链表
思路
指定迭代器起点,通过自增移动当前的下标,依次获取对应的结果,每次返回的是一个KV键值对
操作对象 叶子页面page,当前下标index,缓冲池bpm_
- index为page页内偏移
- 叶子页面有一个指向右兄弟的指针,存储的是Pageid,要取pageId对应的叶子页面需要用到缓冲池
注意点
- 自增可能会移动到右兄弟的页面
- 移动到新页面后,index_从零开始
- 注意加锁解锁
并发索引
实现思路
- 查找
- 只需要加读锁
- 从根往叶子加读锁
- 先获取到孩子节点的锁,再释放自身的锁。
- 插入/删除
- 悲观锁思想:认为会有冲突,所以事先加写锁。
- 从根往叶子加写锁
- 确保当前位置安全再释放,安全的意思是子树即使发生分裂或者合并依然不会对上层造成影响,就释放该及其祖先节点的锁。
- 插入/删除完成后,释放剩余的写锁。
- 插入对于安全的定义:
- 当前节点没满
- 删除对于安全定义:
- 根:删除后,根还有至少一个有效的KV
- 非根:删除后不会引起跟其他节点合并,这个节点要超过半满的状态