MIT6.830的lab5中,我们将实现 B+ 树索引以进行高效查找和范围扫描。
项目已经提供实现树结构所需的所有低级代码。我们所要做的是实现搜索、拆分页面、在页面之间重新分配元组以及合并页面。
主要任务就是:
B+树是B-树的变体,也是一颗多路搜索树。一棵n阶的B+树主要有这些特点:
一颗3阶的B+树如下:
B+树的数据只存储在叶子节点,内部节点值存储索引,通过索引找到相应的叶子节点进行查询
(1)B+树的单值查询
当查询key=70的节点时,首先从读取根节点,判断得key<75;然后读取根节点的左孩子节点,将70依次与左孩子节点中的值进行比较,判断得key>66;则读取66的右孩子节点,key存储于该叶节点中,读取其中的数据。
(2)B+树的范围查询
当要读取[68,100]范围内的数据时,首先找到第一个大于等于68的节点,然后在叶节点中向后遍历。
B+树的插入是从叶子节点中进行的,找到要插入的叶子结点,将数据插入。然后根据插入后叶节点中key的数量进行不同的处理。
分裂叶节点和分裂内节点的情况是不同的。分裂叶节点时,节点中的key值复制到父节点中(即叶节点和内部节点可以有相同的值)
分裂内部节点时,是将节点中的key值“挤到”父节点中(即内部节点之间的key值不能重复)
设:kyesNum = 节点中key的数量;min = ⌈n/2⌉-1 节点能容纳key的最小值,keysNum 7阶B+树,min = 3 (1)叶子节点 keysNum < min ,兄弟节点的keys > min,从兄弟节点处“窃取”key: keysNum < min ,兄弟节点的keys = min,与兄弟节点合并: (2)内部节点 keysNum < min ,兄弟节点的keys > min,从兄弟节点处“窃取”key: keysNum < min ,兄弟节点的keys = min,与兄弟节点合并: 在本lab中,有些基本的类已经给出了,我们只需要调用即可,但这些类,我们也要搞清楚其中的关系。 为方便起见,我们在 BTreePage.java 中创建了一个抽象类,其中包含叶页和内部页共有的代码。此外,标题页在 BTreeHeaderPage.java 中实现,并跟踪文件中的哪些页面正在使用中。最后,每个 BTreeFile 的开头都有一个页面,它指向树的根页面和第一个标题页面。这个单例页面在 BTreeRootPtrPage.java 中实现。熟悉这些类的接口,尤其是BTreePage、BTreeInternalPage 和BTreeLeafPage。您将需要在 B+Tree 的实现中使用这些类。 除了我们要实现的BTreeFile之外,主要就是:BTreePageId、BTreePage、BTreeInternalPage、 BTreeLeafPage、 BTreeHeaderPage、BTreeRootPtrPage、BTreeEntry。 (1)BTreePageId 和在前面lab中的HeapPage一样,每一个BTreePage也都会有一个BTreePageId。 注意下ROOT_PTR、INTERNAL、LEAF、HEADER这四种类型: (2)BTreePage 这是一个抽象类,其中包含BTreeInternalPage和BTreeLeafPage共有的代码。 (3)BTreeInternalPage(继承BTreePage) 其实内部节点可以看成保存着一个个BTreeEntry,内部节点对key的查找、插入、删除、迭代,都是以entry为单位的。通过BTreeEntry可以获取key、LeftChild、RightChild这三种信息,然后存入相应数组中。 (4)BTreeLeafPage(继承BTreePage) 叶节点和内部节点不一样的地方在于,其保存的是一个个真正的数据,也就是保存着一个个Tuple。还有就是其的链表结构用于顺序查找。 (5)BTreeRootPtrPage (6)BTreeHeaderPage 这个page其实就和INTERNAL和LEAF中的byte[] header作用类似,就是当做一种标记记录,来记录每一个pageNo的使用情况,当删除一页时,就可以调用BTreeFile.setEmptyPage()函数,进而调用BTreeHeaderPage.markSlotUsed()函数将header中指定的pageNo设置为unused。 (7)BTreeEntry 内部节点中的entry,内部节点对key的查找、插入、删除、迭代,都是以entry为单位的。 来张图来直观的看一下: BTreeInternalPage底层页面并不存储BTreeEntry,而是存储n-1个key和n个指向子节点的指针(子页的pageNo,父页与子页同处一个table中,知道子页的pageNo就能获取到子页)。 这个exercise主要就是实现BTreeFile中的findLeafPage()方法,实现B+树的查询。 注意这是一个递归函数,在B+树中查找可能包含字段 f 的叶节点。它使用只读权限锁定叶节点路径上的所有内部节点,并使用权限perm锁定叶节点。如果f为null,它将查找最左边的叶节点,用于迭代器。 注意一下 主要判断步骤如下: getPage:调用getPage()获取page时,将检查页面是否已经存储在本地缓存dirtypage中,如果不再,则调用BufferPool.getPage去获取。getPage()如果使用读写权限获取页面,也会将页面添加到dirtypages缓存中,因为它们可能很快就会被弄脏。这种方法的一个优点是,如果在一个元组插入或删除过程中多次访问相同的页面,它可以防止更新丢失。 当我们进行插入Tuple时,可能会将数据插入到一个已经存满了Tuple的叶节点中,这就需要将该叶节点进行拆分,并将所有Tuple平均分配到两个page中(一半留在该page,一半放入另一个page)。 每次叶节点分裂时,都需要将第一页的最后一个元组对应的新条目添加到父节点。有时,内部节点也可能已满,无法接受新条目。在这种情况下,父级应该拆分并向其父级添加一个新条目。这可能会导致递归拆分并最终创建新的根节点。 而基于上诉思想,insertTuple并不需要我们完成,我们要做的任务是完成两个分裂函数: (1)splitLeafPage 当页节点中元组数量等于n时,将其拆分成两个页节点。返回插入tuple所在的page。 getParentWithEmptySlots() 函数是获取一个父结点,并且这个父结点能够插入一个entry。因此这个函数中,可能会涉及内部节点的分裂。 (2)BTreeInternalPage 实现与splitLeafPage类似,区别是分裂之后要将中间的key“挤到” 父节点中去。 删除又是B+树的一个难点,核心在于一个节点被删到小于半满的时候,要么从(丰满的)兄弟那窃取一个元素,要么和另一个也小于半满的(苗条的)兄弟合并。而合并又会导致父节点中的划分元素被删除,运气好的时候父节点又会因为划分元素被删除了,而变成小于半满的苗条状态,然后又是要么从兄弟那窃取一个,要么和不够丰满的兄弟合并。而合并又导致他(父节点)的父节点的划分元素被删除……然后就这么在树上递归了,运气好的话能把Root节点给删了,也就是整个B+树的层数减一。 而在exercise 3中主要是实现三个和窃取有关的函数: (1)stealFromLeafPage (2)stealFromLeftInternalPage (3)stealFromRightInternalPage 在exercise 4中主要是实现两个和page合并有关的函数: (1)mergeLeafPages (2)mergeInternalPages
基本源码介绍
public final static int ROOT_PTR = 0;
public final static int INTERNAL = 1;
public final static int LEAF = 2;
public final static int HEADER = 3;
private final int tableId; //该page所在的的table id
private final int pgNo; //该page所在page的序号(table中的第几个页)
private final int pgcateg; //用于标识BTreePage的类型,也就是ROOT_PTR、INTERNAL、LEAF、HEADER这四种类型
// 脏页标记和事务id
protected volatile boolean dirty = false;
protected volatile TransactionId dirtier = null;
// 索引的大小,也就是每一个指针的大小
// 以页节点为例,每一个页节点都有三个索引指针:left sibling pointer, right sibling pointer, parent pointer
protected final static int INDEX_SIZE = Type.INT_TYPE.getLen();
protected final BTreePageId pid; //当前节点的BTreePageId
protected final TupleDesc td; //当前节点的tuple元数据
protected final int keyField; //索引的关键字字段下标
//当前page的父page,如果是根节点那么就是0
protected int parent; // parent is always internal node or 0 for root node
// 存储旧的数据,oldDataLock是用于并发synchronized的锁
protected byte[] oldData;
protected final Byte oldDataLock= (byte) 0;
private final byte[] header; //记录slot的占用情况
private final Field[] keys; //存储key的数组
private final int[] children; //存储page的序号,用于获取左孩子、右孩子的BTreePageId
private final int numSlots; //不是key的数量,而是内部节点中能存储的指针的数量(即n,内部节点中最多能存储key的数量为n-1)
private int childCategory; //孩子节点的类型(内部节点或叶节点)
private final byte[] header; //记录slot的占用情况
private final Tuple[] tuples; //存储该page的所有tuple
private final int numSlots; //叶节点中能存储的tuple数量(即n-1)
// 页节点的双向链表结构
private int leftSibling; //左兄弟的pageNo,用于获取左兄弟的BTreePageId,为0则没有左兄弟
private int rightSibling; //右兄弟的pageNo,用于获取右兄弟的BTreePageId,为0则没有右兄弟
//这个page的大小
public final static int PAGE_SIZE = 9;
// 脏页标记和事务id
private boolean dirty = false;
private TransactionId dirtier = null;
// 当前节点的BTreePageId
private final BTreePageId pid;
private int root; //保存当前根节点的pageNo
private int rootCategory; //保存当前根节点的类型,INTERNAL或LEAF,当只有一个节点时,就是LEAF
private int header; //保存当前header页的pageNo
private byte[] oldData; //存储旧的数据
// 脏页标记和事务id
private volatile boolean dirty = false;
private volatile TransactionId dirtier = null;
// 索引的大小,也就是每一个指针的大小
final static int INDEX_SIZE = Type.INT_TYPE.getLen();
final BTreePageId pid; //当前节点的BTreePageId
final byte[] header; //记录每一个page的使用情况,对应一个个pageNo
final int numSlots; //记录能存储的page使用情况数量
private int nextPage; // 下一个header page的pageNo,如果是最后一个,就是0
private int prevPage; // 上一个header page的pageNo,如果是第一个,就是0
// 存储旧的数据,oldDataLock是用于并发synchronized的锁
byte[] oldData;
private final Byte oldDataLock= (byte) 0;
/**
* The key of this entry
* */
private Field key; //内部节点中的key
/**
* The left child page id
* */
private BTreePageId leftChild; //左孩子的BTreePageId
/**
* The right child page id
* */
private BTreePageId rightChild; //右孩子的BTreePageId
/**
* The record id of this entry
* */
private RecordId rid; // 标识该entry所在的位置。(即该entry是哪个page中的)
exercise 1 - Search(BTreeFile.findLeafPage())
private BTreeLeafPage findLeafPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreePageId pid, Permissions perm,
Field f)
throws DbException, TransactionAbortedException {
// some code goes here
//1. 如果是叶子节点,直接返回
if(pid.pgcateg() == BTreePageId.LEAF){
return (BTreeLeafPage) getPage(tid,dirtypages,pid,perm);
}
BTreeInternalPage page = (BTreeInternalPage) getPage(tid,dirtypages,pid,perm);
Iterator<BTreeEntry> iterator = page.iterator();
//2. 如果filed为空,找到最左边的节点
if(f==null){
if(iterator.hasNext()){
return findLeafPage(tid,dirtypages,iterator.next().getLeftChild(),perm,f);
}
return null;
}
BTreeEntry next = null;
//3. 否则,内部节点查找符合条件的entry,并递归查找
while(iterator.hasNext()){
next = iterator.next();
Field key = next.getKey();
//当有重复值的时候 节点分裂有可能一半在左边一半在右边,所以是小于等于
if(f.compare(Op.LESS_THAN_OR_EQ,key)){
return findLeafPage(tid,dirtypages,next.getLeftChild(),perm,f);
}
}
//最后一个entry的右子节点
if(next!=null){
return findLeafPage(tid,dirtypages,next.getRightChild(),perm,f);
}
return null;
}
dirtypages:当创建新page或更改page中的数据、指针时,需要将其添加到dirtypages中。exercise 2 - Insert(Splitting Pages)
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.
//1. 将当前page的后半部分放入新page
int half = page.getNumTuples() / 2;
BTreeLeafPage newPage = (BTreeLeafPage) getEmptyPage(tid, dirtypages, BTreePageId.LEAF);
Iterator<Tuple> iterator = page.reverseIterator();
while (iterator.hasNext() && half > 0) {
Tuple next = iterator.next();
//这里要先删除在插入,就很坑
page.deleteTuple(next);
newPage.insertTuple(next);
half--;
}
//2. 获取当前要插入的父节点,并插入新节点
Tuple up = iterator.next();
BTreeInternalPage parentPage = getParentWithEmptySlots(tid, dirtypages, page.getParentId(), field);
BTreeEntry insetEntry = new BTreeEntry(up.getField(keyField), page.getId(), newPage.getId());
parentPage.insertEntry(insetEntry);
//3. 设置节点间的关系
// page newPage rightSibling
if (page.getRightSiblingId() != null) {
BTreeLeafPage right = (BTreeLeafPage) getPage(tid, dirtypages, page.getRightSiblingId(), Permissions.READ_WRITE);
right.setLeftSiblingId(newPage.getId());
dirtypages.put(right.getId(),right);
}
newPage.setRightSiblingId(page.getRightSiblingId());
newPage.setLeftSiblingId(page.getId());
page.setRightSiblingId(newPage.getId());
//4. 增加脏页
dirtypages.put(parentPage.getId(),parentPage);
dirtypages.put(page.getId(),page);
dirtypages.put(newPage.getId(),newPage);
//5. 返回要插入field的页
if (field.compare(Op.GREATER_THAN_OR_EQ, up.getField(keyField))) {
return newPage;
}
return page;
}
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.
//1. 将当前page的后半部分放入新page
int half = page.getNumEntries()/2;
BTreeInternalPage newPage = (BTreeInternalPage) getEmptyPage(tid, dirtypages, BTreePageId.INTERNAL);
Iterator<BTreeEntry> iterator = page.reverseIterator();
while(iterator.hasNext() && half>0){
BTreeEntry next = iterator.next();
page.deleteKeyAndRightChild(next);
newPage.insertEntry(next);
half--;
}
//2. 分裂完,中间的entry插入父节点。注意up节点要在原page中删除,并设置左右子节点。
BTreeEntry up = iterator.next();
page.deleteKeyAndRightChild(up);
up.setLeftChild(page.getId());
up.setRightChild(newPage.getId());
//这里父节点可能还会分裂获取
BTreeInternalPage parentPage = getParentWithEmptySlots(tid, dirtypages, page.getParentId(), field);
parentPage.insertEntry(up);
page.setParentId(parentPage.getId());
newPage.setParentId(parentPage.getId());
//3. 设置newPage子节点的父节点指向
updateParentPointers(tid,dirtypages,newPage);
//4. 增加脏页
dirtypages.put(parentPage.getId(),parentPage);
dirtypages.put(newPage.getId(),newPage);
dirtypages.put(page.getId(),page);
//5. 返回要插入field的页
if (field.compare(Op.GREATER_THAN_OR_EQ, up.getKey())) {
return newPage;
}
return page;
}
exercise 3 - Delete(Redistributing pages)
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. 获取要窃取的tuple个数
int stealNum = ((page.getNumTuples()+sibling.getNumTuples())/2) - page.getNumTuples();
if(stealNum<=0){
return;
}
//2. 判断是从左还是右节点窃取,并获取迭代器
Iterator<Tuple> tupleIterator;
if(isRightSibling){
tupleIterator = sibling.iterator();
}else{
tupleIterator = sibling.reverseIterator();
}
//3. 进行窃取
Tuple next = null;
while(stealNum>0){
next = tupleIterator.next();
sibling.deleteTuple(next);
page.insertTuple(next);
stealNum--;
}
//4. 新建一个entry插入父节点
if(isRightSibling){
entry.setKey(tupleIterator.next().getField(keyField));
}else{
entry.setKey(next.getField(keyField));
}
parent.updateEntry(entry);
}
isRightSibling确定是从左兄弟中“窃取”,还是从右兄弟中“窃取”;entry是父节点中指向page和其兄弟节点的entry,将entry的key更改为两个中右方节点的第一个tuple的关键字。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.
//1. 获取要窃取的tuple个数
int stealNum = (page.getNumEntries()+leftSibling.getNumEntries())/2 - page.getNumEntries();
if(stealNum<=0){
return;
}
//2. 获取迭代器和节点
Iterator<BTreeEntry> leftIterator = leftSibling.reverseIterator();
Iterator<BTreeEntry> pageIterator = page.iterator();
BTreeEntry leftLastEntry = leftIterator.next();
BTreeEntry pageFirstEntry = pageIterator.next();
//3. 先将父节点中的中间entry插入page
BTreeEntry midEntry = new BTreeEntry(parentEntry.getKey(), leftLastEntry.getRightChild(), pageFirstEntry.getLeftChild());
page.insertEntry(midEntry);
stealNum--;
//4. 再插入左节点中的entry
while(stealNum>0){
leftSibling.deleteKeyAndRightChild(leftLastEntry);
page.insertEntry(leftLastEntry);
stealNum--;
leftLastEntry = leftIterator.next();
}
//5. 将左节点最大的entry插入到父节点
leftSibling.deleteKeyAndRightChild(leftLastEntry);
parentEntry.setKey(leftLastEntry.getKey());
parent.updateEntry(parentEntry);
//6. 设置newPage子节点的父节点指向
updateParentPointers(tid,dirtypages,page);
//7. 增加脏页
dirtypages.put(leftSibling.getId(),leftSibling);
dirtypages.put(page.getId(),page);
dirtypages.put(parent.getId(),parent);
}
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.
//1. 获取要窃取的tuple个数
int stealNum = (page.getNumEntries()+rightSibling.getNumEntries())/2 - page.getNumEntries();
if(stealNum<=0){
return;
}
//2. 获取迭代器和节点
Iterator<BTreeEntry> rightIterator = rightSibling.iterator();
Iterator<BTreeEntry> pageIterator = page.reverseIterator();
BTreeEntry rightFirstEntry = rightIterator.next();
BTreeEntry pageLastEntry = pageIterator.next();
//3. 先将父节点中的中间entry插入page
BTreeEntry midEntry = new BTreeEntry(parentEntry.getKey(), pageLastEntry.getRightChild(), rightFirstEntry.getLeftChild());
page.insertEntry(midEntry);
stealNum--;
//4. 再插入右节点中的entry
while(stealNum>0){
rightSibling.deleteKeyAndRightChild(rightFirstEntry);
page.insertEntry(pageLastEntry);
stealNum--;
rightFirstEntry = rightIterator.next();
}
//5. 将右节点最小的entry插入到父节点
rightSibling.deleteKeyAndRightChild(rightFirstEntry);
parentEntry.setKey(rightFirstEntry.getKey());
parent.updateEntry(parentEntry);
//6. 设置newPage子节点的父节点指向
updateParentPointers(tid,dirtypages,page);
//7. 增加脏页
dirtypages.put(rightSibling.getId(),rightSibling);
dirtypages.put(page.getId(),page);
dirtypages.put(parent.getId(),parent);
}
exercise 4 - Delete(Merging pages)
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
//1. 设置右兄弟节点
leftPage.setRightSiblingId(rightPage.getRightSiblingId());
//2. 如果右节点有右兄弟节点,就设置其左节点指向
if(rightPage.getRightSiblingId()!=null){
BTreeLeafPage rightSiblingPage= (BTreeLeafPage) getPage(tid, dirtypages, rightPage.getRightSiblingId(), Permissions.READ_WRITE);
rightSiblingPage.setLeftSiblingId(leftPage.getId());
dirtypages.put(rightSiblingPage.getId(),rightSiblingPage);
}
//3. 进行合并
Iterator<Tuple> iterator = rightPage.iterator();
while(iterator.hasNext()){
Tuple next = iterator.next();
rightPage.deleteTuple(next);
leftPage.insertTuple(next);
}
//4. 设置空page并删除父结点的entry
setEmptyPage(tid,dirtypages,rightPage.getId().getPageNumber());
deleteParentEntry(tid,dirtypages,leftPage,parent,parentEntry);
//5. 增加脏页
dirtypages.remove(rightPage.getId());
dirtypages.put(leftPage.getId(),leftPage);
dirtypages.put(parent.getId(),parent);
}
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
//1. 获取两个节点的迭代器
Iterator<BTreeEntry> leftIterator = leftPage.reverseIterator();
Iterator<BTreeEntry> rightIterator = rightPage.iterator();
BTreeEntry leftLastEntry = leftIterator.next();
BTreeEntry rightFirstEntry = rightIterator.next();
//2. 将两节点中间的父节点entry插入到左节点,并将其删除
BTreeEntry midEntry = new BTreeEntry(parentEntry.getKey(), leftLastEntry.getRightChild(), rightFirstEntry.getLeftChild());
leftPage.insertEntry(midEntry);
deleteParentEntry(tid,dirtypages,leftPage,parent,parentEntry);
//3. 插入右节点的第一个entry
rightPage.deleteKeyAndLeftChild(rightFirstEntry);
leftPage.insertEntry(rightFirstEntry);
//4. 循环插入右节点的entry
while(rightIterator.hasNext()){
rightFirstEntry = rightIterator.next();
rightPage.deleteKeyAndLeftChild(rightFirstEntry);
leftPage.insertEntry(rightFirstEntry);
}
//5. 更新左节点的子节点的父节点指向
updateParentPointers(tid,dirtypages,leftPage);
//6. 设置空page
setEmptyPage(tid,dirtypages,rightPage.getId().getPageNumber());
//7. 增加脏页
dirtypages.remove(rightPage.getId());
dirtypages.put(leftPage.getId(),leftPage);
dirtypages.put(parent.getId(),parent);
}