MIT 6.830数据库系统 -- lab five

MIT 6.830数据库系统 -- lab five

  • 项目拉取
  • 引言
  • 搜索
    • 练习1 BTreeFile.findLeafPage()
  • 插入
    • 练习2 Spliting Page
  • 删除
    • 练习3 页再分配
    • 练习4 合并页
  • 事务
  • 小结


项目拉取

原项目使用ant进行项目构建,我已经更改为Maven构建,大家直接拉取我改好后的项目即可:

  • https://gitee.com/DaHuYuXiXi/simple-db-hw-2021

然后就是正常的maven项目配置,启动即可。各个lab的实现,会放在lab/分支下。


引言

在本实验中,我们将会实现B+树索引用于高效查询和范围扫描。源码中已经提供了基本的树结构,我们需要实现检索、页分裂、在页面之间重新分配元组以及合并页

B+树的内部节点拥有多条记录,每个节点的内容包括节点当前值、以及左右子树的指针;相邻键之间共享一个孩子指针,所以拥有m个键的内部节点通常拥有m+1个孩子指针。叶子节点可以包括数据记录或者指向其他数据库文件的指针。为了简单起见,我们实现的B+树的叶子节点只包括数据记录。相邻的叶子页通过左右同级指针链接在一起,因此范围扫描只需要通过根节点和内部节点进行一次初始搜索即可找到第一个叶子页,后续叶子页通过右(或者左)指针找到。

在这里插入图片描述

  • B+树内部节点是不保存数据的,只作索引作用,它的叶子节点才保存数据。
  • B+树相邻的叶子节点之间是通过链表指针连起来的
  • B+树中,内部节点与其父节点的key值不能重复,叶子节点与其父节点的key值可以重复

下面这幅图是SimpleDB B+ tree这部分整体架构组织图,大家在做下面lab的时候,可以时不时回过来看一眼这张图:
在这里插入图片描述
磁盘上Header Page是懒初始化的,因此出现的位置是不固定的,Internal Page和Leaf Page同样如此,之所以可以这样,是因为存在一个root ptr page,它起到的作用就类似文件系统中的超级块:
在这里插入图片描述

我们应该在lab4的基础上开始本次实验代码的编写,此外,报告中还为本次实验提供了额外的源码和测试文件


搜索

B+树的单值查询

  • 当查询key=70的节点时,首先从读取根节点,判断得key<75;
  • 然后读取根节点的左孩子节点,将70依次与左孩子节点中的值进行比较,判断得key>66;
  • 则读取66的右孩子节点,key存储于该叶节点中,读取其中的数据。

在这里插入图片描述

B+树的范围查询

  • 当要读取[68,100]范围内的数据时,首先找到第一个大于等于68的节点,然后在叶节点中向后遍历。
    在这里插入图片描述

查看index目录下的BTreeFile.java文件,这是实现B+树的核心文件,你将会在该文件为本次实验编写所有代码。不像HeapFile,BTreeFile包含四种不同的页。正如你期望的那样,树的结点有两种不同类型的页面:叶子节点和非叶子节点。

非叶子节点在BTreeInternalPage.java中实现,叶子节点在BTreeLeafPage.java中实现。为了方便起见,BTreePage.java中已经创建了包含叶子节点和非叶子结点共同特性的抽象类。此外,header pages在BTreeHeaderPage.java中实现并且追踪文件中的哪个页面是被使用的。

最后,在每个BTreeFile开始都有一个指向树的根页和第一个header page的页;该单独的页在BTreeRootPtrPage.java中被实现。熟悉这些类的接口,尤其是BTreePage、BTreeInternalPage和BTreeLeafPage。在实现B+树的过程中需要使用它们。

我们的第一个任务就是实现BTreeFile.java中的findLeafPage()函数,该函数通过给定的键查找合适的叶子页,主要用于搜索和插入。例如,假设我们提供了包含两个叶子页的B+树(如图1所示),根节点是一个包含一个键和两个孩子结点指针的内部结点。给定键值1,该函数应该返回第一个叶子结点;同样地,给定键值8,该函数应该返回第二个结点。不太明显的情况是,我们给定键值6,可能存在重复的键,因此两个页结点上可能都包含键6对应的元组。在这种情况下,函数应该返回第一个叶子节点

在这里插入图片描述
我们实现的findLeafPage()函数应该递归的搜索内部节点,直到它到达给定键值对应的叶子页。为了在每阶段找到合适的叶子页,我们应该迭代遍历内部节点的记录斌给比较记录与给定的键值的大小,以确定下一步往哪个方向走。BTreeInternalPage.iterator()使用在BTreeEntry.java中定义的接口提供对内部页面中条目的访问。该迭代器允许我们遍历内部节点的键值,并且访问每个键的左右孩子页指针。当传入的BTreePageId的pgcateg()方法返回值与BTreePageId.LEAF相等时,表明这是一个叶子页。在这种情况下,我们仅需要从缓冲池中获取该页并返回它即可,不需要确保它实际上包含提供的键值f。

当提供的键值是null时,findLeafPage()方法必须处理这种情况。如果给定的值是空的,那么在递归的过程中就遍历最左侧的孩子节点,最终返回最左侧的叶子页。查找最左侧的叶子也对于扫描记录文件非常有用。当查找到正确的叶子页时,我们应该返回它。正如上面提到的那样,我们可以通过pgcateg()方法检查叶子也的类型。我们可以假设只有叶子页和内部节点才会被传递给该函数。

与其直接调用BufferPool.getPage()方法来获取每个内部页面和叶子页,建议调用代码中的包装函数BTreeFile.getPage()。它像BufferPool.getPage()那样工作,但是提供额外的参数去追踪脏页。在接下来的两个练习中,该函数非常重要,在这两个练习中,我们需要实际更新数据,因此需要追踪脏页。

findLeafPage()实现访问的每个内部(非叶)页面都应使用只读权限获取,但返回的叶页面除外,返回的叶页面应使用作为函数参数提供的权限获取。这些权限级别对于本实验室来说并不重要,但对于代码在未来的实验室中正常运行来说,它们将非常重要。


练习1 BTreeFile.findLeafPage()

实现BTreeFile.findLeafPage()方法

通过单元测试BTreeFileReadTest.java和系统测试BTreeScanTest.java意味着完成本次练习

要找到叶子节点可分为如下几种情况:

f为null时:

  1. 每次查询内部节点的最左侧孩子指针指向的节点,直到找到叶子页

f不为null时:

  1. 遍历entry,找到第一个大于要查找的字段f的key,然后递归地调用findLeafPage
  2. 最后如果均不满足条件,那么就直接访问最后一个entry的右孩子指针,递归调用findLeafPage
	private BTreeLeafPage findLeafPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreePageId pid, Permissions perm,Field f)throws DbException, TransactionAbortedException {// some code goes here// 如果f为null,那么直接找到内部节点的最左侧孩子节点指针进行遍历if (null == f) {if (pid.pgcateg() == BTreePageId.LEAF) {return (BTreeLeafPage) getPage(tid, dirtypages, pid, perm);}BTreeInternalPage page = (BTreeInternalPage) getPage(tid, dirtypages, pid, perm);BTreePageId childId = page.getChildId(0);return findLeafPage(tid, dirtypages, childId, perm, f);}//1.获取数据页类型int type = pid.pgcateg();//2.如果是leaf page,递归结束,说明找到了if (type == BTreePageId.LEAF) {return (BTreeLeafPage)getPage(tid, dirtypages, pid, perm);}//3.读取internal page要使用READ_ONLY permBTreeInternalPage internalPage = (BTreeInternalPage)getPage(tid, dirtypages, pid, Permissions.READ_ONLY);//4.获取该页面的entriesIterator<BTreeEntry> it = internalPage.iterator();//这里需要把entry声明在循环外,如果找到最后一个entry还没找到,返回最后一个entry的右孩子BTreeEntry entry = null;while (it.hasNext()) {entry = it.next();Field key = entry.getKey();if (key.compare(Op.GREATER_THAN_OR_EQ, f)) {return findLeafPage(tid, dirtypages, entry.getLeftChild(), perm, f);}}return findLeafPage(tid, dirtypages, entry.getRightChild(), perm, f);}

上述代码实现,事务部分暂不考虑。


插入

为了保证B+树中存储元组的顺序性并且保持B+树的完整性,我们必须将元组插入到包含键范围的叶子页中。正如我们上面提到的,findLeafPage()方法被用于寻找我们应该插入元组的正确的叶子页。但是,每个页都有槽数的限制,即使对应的叶子页已满我们也需要能向其中插入元组。

尝试向已满的叶子页插入元组会导致页分裂,以便元组平均地分布到两个新页中。叶子页的每次分裂,都需要将第二页中的第一个元组对应的新条目添加到父节点。有时,内部节点也可能已满,无法接受新条目。在这种情况下,父节点应该分裂并且向它的父节点添加一个新纪录。这可能导致递归地分裂并且最终创建一个新的根节点

在这里插入图片描述

在本次练习中,我们需要实现BTreeFile.java中的splitLeafPage()和splitInternalPage()方法。如果被分裂的页是根节点,我们需要创建一个新的内部节点作为新的根节点,并且更新BTreeRootPtrPage。否则,我们需要通过READ_WRITE权限读取父页面,如果有必要就递归地进行分裂,并且添加新记录。你会发现getParentWithEmptySlots()函数对于处理这些不同的情况非常有用。在splitLeafPage()方法中我们应该将键复制到父页,而在splitInternalPage()方法中,应该将键推到父页(如图2所示)。记住根据需要更新新页的父指针(为了简单起见,图2没有展示父指针)。
在这里插入图片描述

分裂叶节点时,节点中的key值复制到父节点中(即叶节点和内部节点可以有相同的值)

当一个内部节点被分裂时,我们需要更新被移动的孩子页的父指针。你会发现updateParentPointers()对于这非常有用。此外,记住更新被分裂的叶子页的兄弟指针。最后,返回应该插入新元组或记录的页面,如提供的键字段所示。(提示:不必担心提供的键实际上可能位于要拆分的元组/条目的正中心。应该在拆分期间忽略该键,只使用它来确定返回两个页面中的哪一个)
在这里插入图片描述

分裂内部节点时,是将节点中的key值“挤到”父节点中(即内部节点之间的key值不能重复)

无论何时创建新页面,无论是因为拆分页面还是创建新的根页面,都可以调用getEmptyPage()来或取新页面。这个函数是一个抽象函数,它允许我们重用由于合并而被删除的页面。

我们期望使用BtreeAppPage.iterator()和BTreeInternalPage.iterator()与叶和内部页面交互,以迭代每个页面中的元组/条目。为了方便起见,源码中提供了这两种类型页面的反向迭代器:

  • BTreeLeafPage.reverseIterator() 和 BTreeInternalPage.reverseIterator()。
  • 对于将页中元组/条目的子集移动到其右侧兄弟节点的任务来说,这些反向迭代器非常有用。

如上所述,内部页面迭代器使用BTreeEntry.java中定义的接口,该接口有一个键和两个孩子指针。它也包含一个recordId,用于标识基础页面上键和孩子指针的位置。我们认为一次处理一个条目是与内部页面交互的自然方式,但重要的是要记住,底层页面实际上并不存储条目列表,而是存储m键和m+1子指针的有序列表。由于BTreeEntry只是一个接口,而不是实际存储在页面上的对象,因此更新BTreeEntry的字段不会修改底层页面。为了修改页面上的数据,需要调用BTreeInternalPage.updateEntry()方法。另外,删除一个记录实际上仅仅删除了键和孩子指针,因此源码提供了BTreeInternalPage.deleteKeyAndLeftChild()BTreeInternalPage.deleteKeyAndRightChild()函数来实现这一点。记录的recordId用于查找被删除的键和孩子指针。插入记录也仅仅插入键和孩子指针(除非它是第一条记录),所以BTreeInternalPage.insertEntry()检查所提供的记录中的一个孩子指针是否与页面上现有的孩子指针重叠,并且在该位置插入条目将使键保持排序顺序

在splitLeafPage()和splitINternalPage()方法中,需要使用任何新创建的页面以及由于新指针或新数据而修改的页面来更新dirtypages集合。这是BTreeFile.getPage()派上用场的地方,每次获取页面时,BTreeFile.getPage()都会检查页面是否已经存储在本地缓存中,如果本地缓存中找不到请求的页面,则会从缓冲池中获取该页面。BTreeFile.getPage()如果使用读写权限获取页面,也会将页面添加到dirtypages缓存中,因为它们可能很快就会被弄脏。这种方法的一个优点是,如果在一个元组插入或删除过程中多次访问相同的页面,则可以防止更新丢失。

请注意,与HeapFile.insertTuple()不同的是,BTreeFile.insertTuple()可能会返回大量脏页,特别是在拆分任何内部页的情况下。您可能还记得以前的实验,返回脏页集是为了防止缓冲池在刷新脏页之前逐出脏页

Warning:B+树是一种复杂的数据结构,在修改B+树之前了解每个合法的B+树的必要属性很有帮助:

  1. 如果一个父节点指向孩子节点,那么孩子节点必须指向同一个父节点
  2. 如果叶子节点指向右侧兄弟节点,那么右侧兄弟节点也需要指向左边这个兄弟节点
  3. 第一个叶子和最后一个叶子节点必须分别指向null
  4. 记录ID必须与它们实际属于的页匹配
  5. 具有非叶子节点的节点中key必须大于左子节点中的任何key,小于右子节点中的任何key
  6. 具有叶子节点的节点中key必须大于等于左孩子的所有key,小于等于右孩子的所有key
  7. 节点孩子或为非叶子节点、或为叶子节点
  8. 每个节点最多只有m个子节点,非叶子节点具有至少⌈m/2⌉子节点

在BTreeChecker.java中已经实现了检查上述属性的机制,该方法也用于在 systemtest/BTreeFileDeleteTest.java中测试我们的B+树实现,可以随意添加对该函数的调用,以帮助调试

注意

  1. checker方法应始终在树初始化之后、开始和完成对键插入或删除的完整调用之前和之后通过,但不一定在内部方法中通过。
  2. 树可能格式正确(因此通过checkRep()),但仍然可能不正确。例如,空树始终会通过checkRep()方法,但可能并不总是正确的(如果刚刚插入元组,则树不应该为空)

练习2 Spliting Page

实现BTreeFile.splitLeafPage()和BTreeFile.splitInternalPage()方法

完成本次试验后,我们应该能够通过BTreeFileInsertTest.java单元测试、systemtest/BTreeFileInsertTest.java系统测试。系统测试可能要花费几秒钟才能完成,这些文件会测试我们代码中插入元组和分裂也的正确性,并且处理重复的元组。

完成本练习后,您应该能够通过’BTreeDeadlockTest.java’和’BTreeInsertTest.java’中的单元测试。一些测试用例可能需要几秒钟才能完成BTreeDeadlockTest将测试您是否正确实现了锁定并能够处理死锁BTreeInsertTest将测试代码是否正确插入元组和拆分页面,并处理重复元组和下一个键锁定。

	public BTreeLeafPage splitLeafPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreeLeafPage page, Field field)throws DbException, IOException, TransactionAbortedException {// some code goes here//// Split the leaf page by adding a new page on the right of the existing// page and moving half of the tuples to the new page.  Copy the middle key up// into the parent page, and recursively split the parent as needed to accommodate// the new entry.  getParentWithEmtpySlots() will be useful here.  Don't forget to update// the sibling pointers of all the affected leaf pages.  Return the page into which a // tuple with the given key field should be inserted.// 获取叶子节点元组的数量int numTuples = page.getNumTuples();// 获取一个空的叶子页BTreeLeafPage rightPage = ((BTreeLeafPage) getEmptyPage(tid, dirtypages, BTreePageId.LEAF));// 分裂,将原始叶子页中的一半元素拷贝到空的叶子页中Iterator<Tuple> iterator = page.iterator();int num = numTuples / 2;// 先遍历一半元素while (num > 0) {iterator.next();num--;}// 然后遍历剩余的元组,插入到新的叶子页中,并记录要插入父节点的keyField key = null;while (iterator.hasNext()) {Tuple tuple = iterator.next();// 新页面的第一个元组的key为复制到父节点的keyif (key == null) {key = tuple.getField(page.keyField);}// 从原始的叶子页中删除元组page.deleteTuple(tuple);// 向新页中插入元组rightPage.insertTuple(tuple);}// 更新兄弟指针BTreePageId rightSiblingId = page.getRightSiblingId();if (rightSiblingId != null) {BTreeLeafPage rightSibling = ((BTreeLeafPage) getPage(tid, dirtypages, rightSiblingId, Permissions.READ_WRITE));rightSibling.setLeftSiblingId(rightPage.getId());rightPage.setRightSiblingId(rightSiblingId);dirtypages.put(rightSiblingId, rightSibling);}rightPage.setLeftSiblingId(page.getId());page.setRightSiblingId(rightPage.getId());// 将脏页记录到dirtypages中dirtypages.put(page.getId(), page);dirtypages.put(rightPage.getId(), rightPage);// 向父节点插入新的entryBTreeEntry entry = new BTreeEntry(key, page.getId(), rightPage.getId());BTreeInternalPage parent = getParentWithEmptySlots(tid, dirtypages, page.getParentId(), key);parent.insertEntry(entry);dirtypages.put(parent.getId(), parent);// 由于父页面的变更,更新原始页和新页的父指针updateParentPointer(tid, dirtypages, parent.getId(), page.getId());updateParentPointer(tid, dirtypages, parent.getId(), rightPage.getId());// 判断待插入的key属于哪个叶子页if (field.compare(Op.LESS_THAN_OR_EQ, key)) {return page;} else {return rightPage;}}
	public BTreeInternalPage splitInternalPage(TransactionId tid, Map<PageId, Page> dirtypages,BTreeInternalPage page, Field field) throws DbException, IOException, TransactionAbortedException {// some code goes here//// Split the internal page by adding a new page on the right of the existing// page and moving half of the entries to the new page.  Push the middle key up// into the parent page, and recursively split the parent as needed to accommodate// the new entry.  getParentWithEmtpySlots() will be useful here.  Don't forget to update// the parent pointers of all the children moving to the new page.  updateParentPointers()// will be useful here.  Return the page into which an entry with the given key field// should be inserted.// 记录page中entry的数量int numEntries = page.getNumEntries();// 创建新的BTreeInternalPageBTreeInternalPage internalPage = (BTreeInternalPage) getEmptyPage(tid, dirtypages, BTreePageId.INTERNAL);Iterator<BTreeEntry> iterator = page.reverseIterator();// 将原始页中的一半元素移动到新的内部节点页中int num = numEntries / 2;while (num > 0) {BTreeEntry entry = iterator.next();page.deleteKeyAndRightChild(entry);internalPage.insertEntry(entry);num--;}// 推到父节点的entryBTreeEntry pushEntry = iterator.next();page.deleteKeyAndRightChild(pushEntry);// 记录脏页dirtypages.put(page.getId(), page);dirtypages.put(internalPage.getId(), internalPage);// 更新孩子指针pushEntry.setLeftChild(page.getId());pushEntry.setRightChild(internalPage.getId());// 由于页间元素的移动,更新这些页中元素的孩子指针updateParentPointers(tid, dirtypages, page);updateParentPointers(tid, dirtypages, internalPage);// 父节点,getParentWithEmptySlots会递归地调用splitInternalPage方法BTreeInternalPage parent = getParentWithEmptySlots(tid, dirtypages, page.getParentId(), pushEntry.getKey());parent.insertEntry(pushEntry);dirtypages.put(parent.getId(), parent);updateParentPointers(tid, dirtypages, parent);// 返回entry插入的BTreeInternalPageif (field.compare(Op.LESS_THAN, pushEntry.getKey())) {return page;} else {return internalPage;}}

删除

为了保持树的平衡并且不浪费不必要的空间,B+树的删除操作可能会导致页重新分配元组,最终导致页合并:
在这里插入图片描述

在这里插入图片描述

图3.页再分配

在这里插入图片描述
在这里插入图片描述

​ 图4.页合并

如果试图从小于半满的叶子页中删除元组的话,则会导致该页面从其兄弟节点中窃取元组或与其兄弟节点中的一个合并。如果页面的兄弟节点有多余的元组,则元组应该均匀分布在两个页面之间,并且父级条目应该进行更新(如图3)。但是,如果兄弟节点也是半满(如图4),那么应该合并两个页,并且删除父节点的记录。反过来,从父节点中删除记录也可能导致父节点半满,在这种情况下,父节点应该从它的兄弟节点中窃取记录或者与他的兄弟节点合并。这可能会导致递归地合并,如果根节点的最后一个记录被删除的话,那么最终会删除根节点。

在接下来的练习中我们需要实现BTreeFile.javastealFromLeafPage(),stealFromLeftInternalPage(), stealFromRightInternalPage(),mergeLeafPages()mergeInternalPages() 方法。在前三个函数中,如果兄弟节点有多余的元组/记录,那么我们需要实现均匀地再分布元组/记录。记住更新父节点中相应的key(仔细看图3).在stealFromLeftInternalPage()/stealFromRightInternalPage()方法中,我们需要更新已经被移动的孩子的父节点。我们可以重用updateParentPointers()方法

mergeLeafPages()mergeInternalPages()方法中,我们需要编写合并页的代码,有效地执行splitLeafPage()splitInternalPage()相反操作。deleteParentEntry方法在处理不同的递归情况时非常有用。确保在删除页时调用setEmptyPage()方法以使它们可以被重用。与前面的练习相似,这推荐使用BTreeFile.getPage()方法获取页面并使脏页列表保持最新


练习3 页再分配

实现

  • BTreeFile.stealFromLeafPage()
  • BTreeFile.stealFromLeftInternalPage()
  • BTreeFile.stealFromRightInternalPage()

完成练习后,需要通过单元测试BTreeFileDeleteTest.java (testStealFromLeftLeafPage and
testStealFromRightLeafPage),为了完整地测试系统,测试中可能会创建一个很大的B+树,故系统测试可能需要几秒钟完成。

    public void stealFromLeafPage(BTreeLeafPage page, BTreeLeafPage sibling,BTreeInternalPage parent, BTreeEntry entry, boolean isRightSibling) throws DbException {// some code goes here//// Move some of the tuples from the sibling to the page so// that the tuples are evenly distributed. Be sure to update// the corresponding parent entry.// 1.首先计算需要移动多少元组,然后再进行移动int pageNumTuples = page.getNumTuples();int siblingNumTuples = sibling.getNumTuples();// 如果不满足可窃取条件,那么就直接返回if (siblingNumTuples < pageNumTuples) {return;}Iterator<Tuple> siblingIterator = null;// 如果是右兄弟,那么从第一条记录开始stealif (isRightSibling) {siblingIterator = sibling.iterator();} else {// 如果是左兄弟,从最后一条记录开始stealsiblingIterator = sibling.reverseIterator();}// 要steal的记录条数int moveCount = siblingNumTuples - (pageNumTuples + siblingNumTuples) / 2;while (moveCount > 0) {Tuple tuple = siblingIterator.next();sibling.deleteTuple(tuple);page.insertTuple(tuple);moveCount--;}// 更新entry中的key值Field key = null;if (isRightSibling) {key = siblingIterator.next().getField(sibling.keyField);entry.setKey(key);} else {key = page.iterator().next().getField(page.keyField);entry.setKey(key);}parent.updateEntry(entry);}
    public void stealFromLeftInternalPage(TransactionId tid, Map<PageId, Page> dirtypages,BTreeInternalPage page, BTreeInternalPage leftSibling, BTreeInternalPage parent,BTreeEntry parentEntry) throws DbException, TransactionAbortedException {// some code goes here// Move some of the entries from the left sibling to the page so// that the entries are evenly distributed. Be sure to update// the corresponding parent entry. Be sure to update the parent// pointers of all children in the entries that were moved.// 计算需要移动的元素个数int pageNumEntries = page.getNumEntries();int siblingNumEntries = leftSibling.getNumEntries();int moveCount = siblingNumEntries - (pageNumEntries + siblingNumEntries) / 2;Iterator<BTreeEntry> siblingIterator = leftSibling.reverseIterator();// 先处理parentEntry和leftSibling的倒数第一个节点,注意左右孩子指针的更新BTreeEntry right = page.iterator().next();BTreeEntry left = siblingIterator.next();BTreeEntry entry = new BTreeEntry(parentEntry.getKey(), left.getRightChild(), right.getLeftChild());page.insertEntry(entry);moveCount--;// 移动元素while (moveCount > 0 && siblingIterator.hasNext()) {leftSibling.deleteKeyAndRightChild(left);page.insertEntry(left);left = siblingIterator.next();moveCount--;}// 更新parent的entryleftSibling.deleteKeyAndRightChild(left);parentEntry.setKey(left.getKey());parent.updateEntry(parentEntry);updateParentPointers(tid, dirtypages, page);}
    public void stealFromRightInternalPage(TransactionId tid, Map<PageId, Page> dirtypages,BTreeInternalPage page, BTreeInternalPage rightSibling, BTreeInternalPage parent,BTreeEntry parentEntry) throws DbException, TransactionAbortedException {// some code goes here// Move some of the entries from the right sibling to the page so// that the entries are evenly distributed. Be sure to update// the corresponding parent entry. Be sure to update the parent// pointers of all children in the entries that were moved.// 计算移动元素的个数int curEntries = page.getNumEntries();int rightSiblingNumEntries = rightSibling.getNumEntries();int moveCount = rightSiblingNumEntries - (curEntries + rightSiblingNumEntries) / 2;Iterator<BTreeEntry> iterator = rightSibling.iterator();// 首先处理parentEntry和右侧兄弟节点的第一个entryBTreeEntry right = iterator.next();BTreeEntry left = page.reverseIterator().next();BTreeEntry entry = new BTreeEntry(parentEntry.getKey(), left.getRightChild(), right.getLeftChild());page.insertEntry(entry);moveCount--;// 移动元素while (moveCount > 0 && iterator.hasNext()) {rightSibling.deleteKeyAndLeftChild(right);page.insertEntry(right);right = iterator.next();moveCount--;}// 更新parent的entryrightSibling.deleteKeyAndLeftChild(right);parentEntry.setKey(right.getKey());parent.updateEntry(parentEntry);updateParentPointers(tid, dirtypages, page);}

练习4 合并页

实现BTreeFile.mergeLeafPages()BTreeFile.mergeInternalPages()方法

此时我们可以通过单元测试BTreeFileDeleteTest.java和系统测试systemtest/BTreeFileDeleteTest.java

    public void mergeLeafPages(TransactionId tid, Map<PageId, Page> dirtypages,BTreeLeafPage leftPage, BTreeLeafPage rightPage, BTreeInternalPage parent, BTreeEntry parentEntry)throws DbException, IOException, TransactionAbortedException {// some code goes here//// Move all the tuples from the right page to the left page, update// the sibling pointers, and make the right page available for reuse.// Delete the entry in the parent corresponding to the two pages that are merging -// deleteParentEntry() will be useful here// 移动tupleIterator<Tuple> iterator = rightPage.iterator();while (iterator.hasNext()) {Tuple tuple = iterator.next();rightPage.deleteTuple(tuple);leftPage.insertTuple(tuple);}// 修改左右指针BTreePageId rightSiblingId = rightPage.getRightSiblingId();if (rightSiblingId != null) {// 兄弟节点BTreeLeafPage page = (BTreeLeafPage) getPage(tid, dirtypages, rightSiblingId, Permissions.READ_WRITE);leftPage.setRightSiblingId(rightSiblingId);page.setLeftSiblingId(leftPage.getId());} else {leftPage.setRightSiblingId(null);}// 善后工作:将rightPage置空以便重用,并删除parentEntrysetEmptyPage(tid, dirtypages, rightPage.getId().getPageNumber());deleteParentEntry(tid, dirtypages, leftPage, parent, parentEntry);}
    public void mergeInternalPages(TransactionId tid, Map<PageId, Page> dirtypages,BTreeInternalPage leftPage, BTreeInternalPage rightPage, BTreeInternalPage parent, BTreeEntry parentEntry)throws DbException, IOException, TransactionAbortedException {// some code goes here//// Move all the entries from the right page to the left page, update// the parent pointers of the children in the entries that were moved,// and make the right page available for reuse// Delete the entry in the parent corresponding to the two pages that are merging -// deleteParentEntry() will be useful here// 先复制parentEntry的key值并设置指针,插入左页面BTreeEntry lastEntry = leftPage.reverseIterator().next();BTreeEntry firstEntry = rightPage.iterator().next();BTreeEntry bTreeEntry = new BTreeEntry(parentEntry.getKey(), lastEntry.getRightChild(), firstEntry.getLeftChild());leftPage.insertEntry(bTreeEntry);// 移动元素Iterator<BTreeEntry> iterator = rightPage.iterator();while (iterator.hasNext()) {BTreeEntry entry = iterator.next();rightPage.deleteKeyAndLeftChild(entry);leftPage.insertEntry(entry);}// 善后工作:将rightPage置空以便重用setEmptyPage(tid, dirtypages, rightPage.getId().getPageNumber());updateParentPointers(tid, dirtypages, leftPage);deleteParentEntry(tid, dirtypages, leftPage, parent, parentEntry);}

事务

通过next-key lock,B+树可以防止在两次连续范围扫描之间出现幻读的问题。由于SimpleDB使用页面级、严格的两阶段锁定,因此如果B+树实现正确的话,那就可以有效地防止幻读发生。因此,我们的B+树实现代码应该通过BTreeNextKeyLockingTest测试.

此外,如果我们在B+树代码中正确地实现锁,那么我们的代码也应该通过单元测试test/simpledb/BTreeDeadlockTest.java

如果所有练习都正确地实现,那么我们应该能够通过BTreeTest系统测试。通过该测试可能需要几分钟的时间。


小结

本节详细代码可以参考仓库lab five分支,相关类核心源码注释都已给出,大部分是笔者个人拙见,难免有错,希望大家带着辩证的视角去看待。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/14980.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Zookeeper入门介绍

Zookeeper在我本次系统的学习之前是已经开始使用了&#xff0c;但是并不理解Zookeeper到底是什么&#xff0c;有什么作用&#xff0c;你或许跟我有一样的疑惑&#xff0c;本专栏将会解决这些疑惑。 目录 Zookeeper介绍&#xff1a; zookeeper特点&#xff1a; 数据结构&#x…

《MySQL 实战 45 讲》课程学习笔记(二)

日志系统&#xff1a;一条 SQL 更新语句是如何执行的&#xff1f; 与查询流程不一样的是&#xff0c;更新流程还涉及两个重要的日志模块&#xff1a;redo log&#xff08;重做日志&#xff09;和 binlog&#xff08;归档日志&#xff09;。 重要的日志模块&#xff1a;redo l…

【VSCode部署模型】导出TensorFlow2.X训练好的模型信息

参考tensorflow2.0 C加载python训练保存的pb模型 经过模型训练及保存&#xff0c;我们得到“OptimalModelDataSet2”文件夹&#xff0c;模型的保存方法(.h5或.pb文件)&#xff0c;参考【Visual Studio Code】c/c部署tensorflow训练的模型 其中“OptimalModelDataSet2”文件夹保…

Doris安装部署入门

文章目录 一 Doris 介绍1.1 使用场景1.1.2 Doris架构 二 Doris单机部署2.1 下载 Doris2.2 配置 Doris2.2.1 配置 FE2.2.2 启动 FE2.2.3 查看 FE 运行状态2.2.4 连接 FE2.2.5 停止 FE 节点2.2.6 配置 BE2.2.7 启动 BE2.2.8 添加 BE 节点到集群2.2.9 查看 BE 运行状态2.2.10 停止…

GitHub仓库如何使用

核心&#xff1a;GitHub仓库如何使用 目录 1.创建仓库&#xff1a; 2.克隆仓库到本地&#xff1a; 3.添加、提交和推送更改&#xff1a; 4.分支管理&#xff1a; 5.拉取请求&#xff08;Pull Requests&#xff09;&#xff1a; 6.合并代码&#xff1a; 7.其他功能&…

网络知识整理

网络知识整理 网络拓扑网关默认网关 数据传输拓扑结构层面协议层面 网络拓扑 网关 连接两个不同的网络的设备都可以叫网关设备&#xff0c;网关的作用就是实现两个网络之间进行通讯与控制。 网关设备可以是交换机(三层及以上才能跨网络) 、路由器、启用了路由协议的服务器、代…

k8s Webhook 使用java springboot实现webhook 学习总结

k8s Webhook 使用java springboot实现webhook 学习总结 大纲 基础概念准入控制器&#xff08;Admission Controllers&#xff09;ValidatingWebhookConfiguration 与 MutatingWebhookConfiguration准入检查&#xff08;AdmissionReview&#xff09;使用Springboot实现k8s-Web…

Linux 学习记录57(ARM篇)

Linux 学习记录57(ARM篇) 本文目录 Linux 学习记录57(ARM篇)一、外部中断1. 概念2. 流程图框 二、相关寄存器1. GIC CPU Interface (GICC)2. GIC distributor (GICD)3. EXTI registers 三、EXTI 寄存器1. 概述2. 内部框图3. 寄存器功能描述4. EXTI选择框图5. EXTI_EXTICR1 &…

【kubernetes系列】flannel之vxlan模式原理

概述 在Kubernetes中要保证容器之间网络互通&#xff0c;网络至关重要。而Kubernetes本身并没有自己实现容器网络&#xff0c;而是而是借助CNI标准&#xff0c;通过插件化的方式自由接入进来。在容器网络接入进来需要满足如下基本原则&#xff1a; Pod无论运行在任何节点都可…

九、HAL_IWDG独立看门狗的使用

1、开发环境 (1)Keil MDK: V5.38.0.0 (2)STM32CubeMX: V6.8.1 (3)MCU: STM32F407ZGT6 2、IWDG简介 (1)IWDG即独立看门狗。 (2)看门狗本质上是一个定时器&#xff0c;设置一个时间&#xff0c;时间到即让程序复位。所以需要在在时间未到之前重置定时器&#xff0c;也就是喂…

JavaWeb开发(后端Web开发【一】)

文章目录 前言一、Maven1.Maven概述-介绍1.1.Maven概述-介绍1.2.Maven概述-安装 2.IDEA集成Maven2.1.IDEA集成Maven-配置Maven环境2.2.IDEA集成Maven-创建Maven项目2.3.IDEA集成Maven-导入Maven项目 3.Maven-依赖管理3.1.Maven-依赖管理-依赖配置3.2.Maven-依赖管理-依赖传递3.…

MySQL5.7 与 MariaDB10.1 审计插件兼容性验证

这是一篇关于发现 MariaDB 审计插件导致 MySQL 发生 crash 后&#xff0c;展开适配验证并进行故障处理的文章。 作者&#xff1a;官永强 爱可生DBA 团队成员&#xff0c;擅长 MySQL 运维方面的技能。热爱学习新知识&#xff0c;亦是个爱打游戏的宅男。 本文来源&#xff1a;原创…

进阶高级测试专项,Pytest自动化测试框架总结(三)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、pytest前置条件…

常见面试题之设计模式--责任链模式

1. 概述 在现实生活中&#xff0c;常常会出现这样的事例&#xff1a;一个请求有多个对象可以处理&#xff0c;但每个对象的处理条件或权限不同。例如&#xff0c;公司员工请假&#xff0c;可批假的领导有部门负责人、副总经理、总经理等&#xff0c;但每个领导能批准的天数不同…

【外卖系统】分类管理业务

公共字段自动填充 需求分析 对于之前的开发中&#xff0c;有创建时间、创建人、修改时间、修改人等字段&#xff0c;在其他功能中也会有出现&#xff0c;属于公共字段&#xff0c;对于这些公共字段最好是在某个地方统一处理以简化开发&#xff0c;使用Mybatis Plus提供的公共…

小程序新渲染引擎 Skyline 发布正式版

为了进一步提升小程序的渲染性能和体验&#xff0c;我们推出了一套新渲染引擎 Skyline&#xff0c;现在&#xff0c;跟随着基础库 3.0.0 发布 Skyline 正式版。 我们知道&#xff0c;小程序一直用 WebView 来渲染界面&#xff0c;因其有不错的兼容性和丰富的特性&#xff0c;且…

PM2.5传感器(PMS5003)STM32代码

PM2.5传感器型号&#xff1a;PMS5003 PMS5003简介如下&#xff1a; 详情&#xff1a;PMS5003资料链接 PM2.5传感器代码下载&#xff0c;本人所写&#xff0c;亲测有效&#xff0c;基于STM32F407(其他STM32型号皆可移植&#xff0c;只需修改UART参数即可),UART打印数据

Git的常用命令以及使用场景

文章目录 1.前言2.工作区,暂存区,版本库简介3.Git的常用命令4.版本回退5.撤销修改6.删除文件7.总结 1.前言 在学习Git命令之前,需要先了解工作区,暂存区和版本库这三个概念 2.工作区,暂存区,版本库简介 在使用Git进行版本控制时&#xff0c;有三个重要的概念&#xff1a;工作…

基于Truss+Docker+Kubernetes把开源模型Falcon-7B送上云端(译)

背景 到目前为止&#xff0c;我们已经看到了ChatGPT的能力及其所能提供的强大功能。然而&#xff0c;对于企业应用来说&#xff0c;像ChatGPT这样的封闭源代码模型可能会带来风险&#xff0c;因为企业自身无法控制他们的数据。尽管OpenAI公司声称用户数据不会被存储或用于训练…

【1.1】Java微服务:初识微服务

✅作者简介&#xff1a;大家好&#xff0c;我是 Meteors., 向往着更加简洁高效的代码写法与编程方式&#xff0c;持续分享Java技术内容。 &#x1f34e;个人主页&#xff1a;Meteors.的博客 &#x1f49e;当前专栏&#xff1a; 微服务 ✨特色专栏&#xff1a; 知识分享 &#x…