通过几天鏖战终于完成了lab2,本lab实现一个支持并发操做的B+树。简直B格满满。node
B+树本质上是一个索引数据结构。好比咱们要用某个给定的ID去检索某个student记录,若是没有索引的话,咱们可能从第一条记录开始遍历每个student记录,直到找到某个ID和咱们给定的ID一致的记录。可想而知,这是很是耗时的。
若是咱们已经维护了一个以ID为KEY的索引结构,咱们能够向索引查询这个ID对应的记录所在的位置,而后直接从这个位置读取这个记录。从索引查询某个ID对应的位置,这个操做须要高效,B+树能保证以O(log n)的时间复杂度完成。git
B+树由叶子节点和内部节点组成,和其它树结构差很少,可是对(KEY, VALUE)的个数和排列顺序有要求。github
格式以下:算法
* --------------------------------------------------------------------------- * | HEADER | KEY(1) + RID(1) | KEY(2) + RID(2) | ... | KEY(n) + RID(n) * ---------------------------------------------------------------------------
假设叶子结点最多能容纳个n个(KEY, RID)对,那么该叶子节点任什么时候候都不能少于n/2向上取整个(KEY, RID)对。假设(KEY, RID)对个数为x,那么x必须知足:安全
ceil(n/2) <= x <= n
ceil表示向上取整,博客园不支持LaTeX o(╯□╰)o。
KEY是search key,RID是该KEY对应的记录的位置。(KEY, RID)对按照KEY的増序进行排列。
HEADER的结构以下:数据结构
* ---------------------------------------------------------------------------------------- * | PageType (4) | LSN (4) | CurrentSize (4) | MaxSize (4) | ParentPageId (4) | PageId(4) | * ---------------------------------------------------------------------------------------
ParentPageId指向父节点。并发
* ---------------------------------------------------------------------------------------- * | HEADER | INVALID_KEY+PAGE_ID(1) | KEY(2)+PAGE_ID(2) | ... | KEY(n)+PAGE_ID(n) | * ----------------------------------------------------------------------------------------
假设内部节点最多容纳n个(KEY, PAGE_ID)对,和叶子节点同样,x必须知足:函数
ceil(n/2) <= x <= n
KEY表示search key,PAGE_ID指的是子节点的ID。
(KEY, PAGE_ID)对按照KEY的増序进行排列。
第一个KEY是无效的。
假设PAGE_ID(i)对应的子树中的KEY用SUB_KEY表示,那么SUBKEY都知足:KEY(i) <= SUB_KEY < KEY(i+1)。
测试
课本p489给出了find的伪代码。总结来讲就是先找到KEY应该出现的叶子节点,而后在该叶子节点中,查找KEY对应的RID。
以下图:
假如咱们但愿查找的KEY为38,第一步在根节点A查找38应该出如今哪一个子节点中,根据以前的性质,38应该出如今以B为根的子树中,继续查找节点B,以此类推,最终38应该出如今H的叶子节点中。最后咱们在H中查找38。
因此对于内部节点,咱们须要一个Lookup(const KeyType &key,const KeyComparator &comparator)方法,查找key应该出如今哪一个子节点对应的子树中。线程
INDEX_TEMPLATE_ARGUMENTS ValueType B_PLUS_TREE_INTERNAL_PAGE_TYPE::Lookup(const KeyType &key, const KeyComparator &comparator) const { assert(GetSize() >= 2); // 先找到第一个array[index].first大于等于key的index(从index 1开始) int left = 1; int right = GetSize() - 1; int mid; int compareResult; int targetIndex; while (left <= right) { mid = left + (right - left) / 2; compareResult = comparator(array[mid].first, key); if (compareResult == 0) { left = mid; break; } else if (compareResult < 0) { left = mid + 1; } else { right = mid - 1; } } targetIndex = left; // key比array中全部key都要大 if (targetIndex >= GetSize()) { return array[GetSize() - 1].second; } if (comparator(array[targetIndex].first, key) == 0) { return array[targetIndex].second; } else { return array[targetIndex - 1].second; } }
由于KEY是已排序的,因此能够先二分查找第一个大于或等于KEY的下标targetIndex,若是targetIndex对应的KEY就是咱们要找的KEY,那么targetIndex对应的value就是下一步要搜索的节点,不然targetIndex-1对应的value是下一步应该搜索的节点。
课本p494给出了完整的insert(key, value)操做的伪代码。
思路就是:
课本p498给出了完整的delete(key)操做的伪代码。
思路:
最粗暴的方式就是在find, insert, delete开始就加锁,执行完毕后解锁,这样逻辑上没有问题,可是并发效率很低,至关于串行执行。
该协议容许多个线程同时访问修改B+树。
举个查找过程的例子,查找key=38:
举个插入过程的例子,插入25:
crab有螃蟹的意思,了解完crabbing协议加锁的过程,应该不难理解为何叫crabbing协议了吧。
咱们须要保护根节点id。
考虑下面这种状况:
两个线程同时执行插入操做,插入前B+树只有一个节点,线程一插入当前key后将分裂,生成一个新的根节点。另外一个线程在线程一分裂前读取了旧的根节点,从而将key插入到了错误的叶子节点中。
解决办法:
在访问,修改root_page_id_的地方加锁,访问或者修改完毕root_page_id_后释放锁。root_page_id_指向的是该B+树的根节点,会保存在内存中,以便快速查找。