# Query

​ 查询部分较为简单,总体思路是:先获取头表 header_page,然后获取树根 root_page,再用树根不断变换成内结点,最后找到叶结点的相应位置。在每个结点内部只要写一个二分查找就行。

INDEX_TEMPLATE_ARGUMENTS
auto BPLUSTREE_TYPE::Find(const InternalPage *page, const KeyType &key) -> int {
  int l = 0;
  int r = page->GetSize() - 1;
  while (l < r) {
    int mid = (l + r + 1) >> 1;
    if (comparator_(page->KeyAt(mid), key) != 1) {
      l = mid;
    } else {
      r = mid - 1;
    }
  }
  if (r == -1 && comparator_(page->KeyAt(r), key) == 1) {
    r = 0;
  }

​ 上述是内结点二分的代码,和叶节点的二分有点区别,因为内结点第一个 key 是 invalid 的。大概结构的区别如下:

​ 贴一段教材中的单点查询流程:

​ 过程中只拿读锁,并且走到子结点就立即释放父结点的读锁。

# Insert

​ 插入新值较为麻烦,可能会有结点的分裂,需要递归向上层传递 key。按照步骤写就行,注意边界处理。写了分类内部结点、叶节点、乐观插入、悲观插入四个函数,其中分裂内部结点的代码如下:

INDEX_TEMPLATE_ARGUMENTS
auto BPLUSTREE_TYPE::SplitInternal(InternalPage *internal, const KeyType &key, page_id_t *new_id,
                                   page_id_t new_child_id) -> KeyType {
  //new_child_id 为下层分裂出的新结点 id, 即将 up_key 上传的页面
  bool put_left = false; // 放在左边还是右边
  int mid = internal->GetMaxSize() / 2;
  KeyType up_key = key;
  auto mid_key = internal->KeyAt(mid);
  // internal:  arr[0].key==invalid,val=first_child
  if (comparator_(mid_key, key) == -1) {
    up_key = comparator_(internal->KeyAt(mid + 1), key) == -1 ? internal->KeyAt(mid + 1) : key;
    mid++;
  } else {
    up_key = internal->KeyAt(mid);
    put_left = true;
  }
  page_id_t up_key_id = internal->ValueAt(mid);
  // 如果 up_key 不是从下层传上来的 key, 则该层删除 up_key
  if (comparator_(up_key, key) != 0) {
    for (int i = mid; i < internal->GetSize() - 1; i++) {
      internal->SetAt(i, internal->KeyAt(i + 1), internal->ValueAt(i + 1));
    }
    internal->IncreaseSize(-1);
  }
  auto new_internal_basic_guard = bpm_->NewPageGuarded(new_id);
  new_internal_basic_guard.SetDirty(true);
  new_internal_basic_guard.Drop();
  auto new_internal_guard = bpm_->FetchPageWrite(*new_id);
  auto new_internal = new_internal_guard.AsMut<InternalPage>();
  new_internal->Init(internal_max_size_);
  int internal_size = internal->GetSize();
  // 移动
  for (int i = mid, j = 1; i < internal_size; i++, j++) {
    new_internal->SetAt(j, internal->KeyAt(i), internal->ValueAt(i));
    new_internal->IncreaseSize(1);
    internal->IncreaseSize(-1);
  }
  // 实际 size 比孩子数量多 1, 因为第一个 key 为 invalid
  new_internal->IncreaseSize(1);
  if (comparator_(up_key, key) != 0) {
    // where to lay key
    auto put_in_internal = internal;
    if (!put_left) {
      put_in_internal = new_internal;
    }
    int idx = Find(put_in_internal, key);
    for (int i = put_in_internal->GetSize(); i > idx + 1; i--) {
      put_in_internal->SetAt(i, put_in_internal->KeyAt(i - 1), put_in_internal->ValueAt(i - 1));
    }
    put_in_internal->SetAt(idx + 1, key, new_child_id);
    // lay the key at right
    if (!put_left && put_in_internal->GetSize() == 0) {
      put_in_internal->SetSize(2);
    } else {
      put_in_internal->IncreaseSize(1);
    }
    // up_key!=key
    new_internal->SetAt(0, KeyType{}, up_key_id);
  } else {
    // up_key==key
    new_internal->SetAt(0, KeyType{}, new_child_id);
  }
  new_internal_guard.SetDirty(true);
  new_internal_guard.Drop();
  return up_key;// 向上传递插入的 key
}

​ 附上一段 TEST2 的树结构,刚开始一直莫名其妙地没过。。。

​ 在插入时可以先乐观插入,一个结点获取下一个结点的读锁后就把父结点的读锁释放,到叶结点再拿写锁。如果中途发现结点要分类,则终止插入,进行悲观插入,向下找值的时候一路拿读锁(头页可以看根结点 kv 对数量直接判断会不会分裂,从而决定要不要先把锁放了),全部改完再将锁释放。

# Delete

删除是逻辑最繁琐的,涉及到结点合并,父结点和兄弟结点旋转等操作。

总体思路:

(1)从上至下找到要删除的值,记录走过的结点中的 key,找不到直接退出

(2)先将该值删除,判断是否小于 MinSize,若不小于则直接退出

(3)结点剩余 key 数量过少,若有左兄弟且左兄弟 key 数量大于 MinSize,先从左兄弟借一个,否则判断右兄弟从右兄弟借一个

(4)如果左右兄弟均不可借,若有左兄弟则将当前结点和左兄弟融合,否则和右兄弟融合

(5)若发生融合,父节点要对应变化,内结点可能合并,递归处理内结点:若当前结点和左兄弟融合,先将对应的父结点的走过的 key 下放到左兄弟末尾,然后将当前结点第一个值放到父结点刚下放的 key 处(可能为空),然后将当前结点剩余的所有值移动到左兄弟;若当前节点和右兄弟融合,先将对应的父结点走过的 key 的后一个下放到当前节点末尾,然后将右兄弟第一个结点放到父结点刚下放的 key 处,然后将右兄弟剩余的所有值移动到当前结点

贴一段融合内部结点和其左兄弟的代码:

// merge internal
		// merge into left
        if (parent_index > 0) {
          auto left_id = parent_internal->ValueAt(parent_index - 1);
          auto left_guard = bpm_->FetchPageWrite(left_id);
          auto left_internal = left_guard.template AsMut<InternalPage>();
          left_internal->SetAt(left_internal->GetSize(), parent_internal->KeyAt(parent_index),
                               cur_internal->ValueAt(0));
          left_internal->IncreaseSize(1);
          // move current to left internal sib
          for (int i = left_internal->GetSize(), j = 1; j < cur_internal->GetSize(); i++, j++) {
            left_internal->SetAt(i, cur_internal->KeyAt(j), cur_internal->ValueAt(j));
            left_internal->IncreaseSize(1);
          }
          // delete parent_index
          for (int i = parent_index; i < parent_internal->GetSize() - 1; i++) {
            parent_internal->SetAt(i, parent_internal->KeyAt(i + 1), parent_internal->ValueAt(i + 1));
          }
          parent_internal->IncreaseSize(-1);
		 // release lock
          cur_guard.SetDirty(false);
          cur_guard.Drop();
          left_guard.SetDirty(true);
          left_guard.Drop();
          cur_guard = std::move(parent_guard);
          cur_internal = parent_internal;
        } else {
        	//merge into right
        	//...
        }

# Iterator

较为简单,IndexIterator 类中添加的成员变量如下:

重载运算符只要沿着叶结点不断向右移动即可,判断一下边界。重载 == 和!= 注意不要自递归了…

# Concurrent Index

策略:

(1)查询时只获取读锁,使用后立即释放

(2)插入时先乐观插入,查找值只获取读锁,使用后立即释放,到叶结点再获取写锁,若发现结点分裂再悲观插入,一路获取写锁,中途判断某结点一定不会分裂则将其和其祖先的锁全都释放

(3)删除时先乐观删除,只获取读锁,使用后立即释放,到叶结点再获取写锁,若发现结点融合,再重新插入一遍,此时一直获取写锁,中途同样可以判断会不会融合释放一定数量的锁。

# Test

// 除了并发控制的测试都过了,一直提示有内存泄露问题

2023.9.14 update // 调试了一周了,并发还是过不了,不想改近 2000 行的 b + 树屎山代码了,换个 fall2022 版本重新写一遍吧,怀疑是 page_guard 没衔接好,但 page_guard 却能通过在线测试。。。

2023.9.16 update // 反复腾跳之后还是继续写 spring2023 吧,居然调通了,最后发现问题在 lru-k???但是它能过在线测试,明明只加了一把大锁这也能出并发问题?!

更新于