Berkeley DB 1.8.6源代码学习(五)

bt_delete.c 文件:删除 B 树中的数据;

__bt_delete 函数:删除指定的记录;

int   __bt_delete(dbp, key, flags)

      const DB *dbp;

      const DBT *key;

         u_int flags;

参数列表:

dbp :数据库指针;

key :待删除记录的键值( DBT );

flags :删除方式, key 指定的记录还是当前游标;

返回值:成功、失败、不确定( key 未找到);

伪代码

获取数据库的 B t

取出 t PIN 页面;

如果 r 是只读树,则返回失败;

如果 flags 0 ,即删除键值为 key 的记录,则使用 __bt_bdelete 函数删除,成功删除后设置 t 已经修改标志,返回;

如果 flags R_CURSOR ,即删除游标指定的记录,游标必须初始化,并且有效;

首先获取 t 的游标 c ;如果 c 没有初始化,则返回失败;

如果 c 被设置为 CURS_ACQUIRE CURS_AFTER CURS_BEFORE ,表示当前的游标不可用,则返回不确定;

获取游标所在的页面 h

如果 h 只含有一条记录,即删除游标指向的记录后 h 为空,此时需要删除 h ,删除页面需要有回溯用的祖先栈,使用 __bt_stkacq 重建祖先栈用于删除记录后删除页面之用;

使用 __bt_dleaf 函数删除游标指向的记录;

如果删除游标之后 h 为中页面,则使用 __bt_pdelete 函数删除 h ,否则将 h 置为需要写回标志 MPOOL_DIRTY

 

__bt_stkacq 函数:为删除游标入口项准备祖先栈;

static int   __bt_stkacq(t, hp, c)

         BTREE *t;

      PAGE **hp;

      CURSOR *c;

参数列表:

t :树指针;

hp :当前页面指针的指针,用于传入当前页面;

c :游标;

返回值: 0 表示成功, 1 表示失败;

伪代码

获取 hp 指向的页面 h

取出 h PIN 标记;

使用 __bt_search 函数在树中查找游标指向记录的键值;如果找不到,则返回失败;否则返回找到的记录位置 e

e 所在的页面赋给 h

如果 h 的编号与游标指向页面的编号相等,则取出 h PIN 标记,将 h 页下载的缓冲池中并通过 hp 返回;注意,本函数的功能是重构祖先栈,所以只要通过 __bt_search 函数找到游标指向的页面即可,此时 __bt_search 已经重构的 B 树的遍历栈;

如果 h 的页面编号不等于游标指向的页面编号,则说明当前树支持复键,且 __bt_search 函数找到的仅仅是匹配游标指向记录键值的众多记录中的一个,而没有定位到游标所在的页面;此时需要向右继续搜寻,直到找到游标所在的页面,在此过程中可能需要依照祖先栈回溯,才能移动到对应的右兄弟页面;具体操作如下:

1 、如果 h 的右兄弟 nextpg 为空,转 10

2 、去除 h PIN 标记,准备回溯,初始化回溯次数 level 0

3 、使用 BT_POP 宏从栈中获取 h 的父记录 parent

4 、获取 h 的父结点页面,将其赋值给 h ,即 h 向上回溯;

5 、如果 parent 记录不为最后一条索引,则将 parent 的下一条记录压入栈中;否则,去除 h PIN 标记,将回溯次数 level 递增;转 3

6 、如果 level 不等于 0 ,当前栈指向的记录为内结点记录,则将 level 递减;否则转 8

7 、获取当前栈指向的记录 bi ,将 bi 所代表的页面入站;去除 h PIN 标志,将 bi 代表的页面取出赋给 h ,即栈增长,遍历下移;将 h 的第一条记录作为栈的当前记录,因为复键的记录如果不在统一个页面上,在必然在相邻页面的边缘处,可以参见页面分裂的策略;然后转 6 ,继续向下遍历;

8 、取出 h PIN 标记;获取 nextpg 页面赋给 h

9 、如果 h 的页面编号与游标指向的页面编号不相等,转 1 ;否则,转 10

10 、如果 h 的页面编号与游标指向的页面编号相等,则去除 h PIN 标记,将 h 页下载的缓冲池中并通过 hp 返回;

如果上述操作不能得到成功的返回,可能是 __bt_search 函数在查找是没有将位置停留在复键第一条记录上,此时需要向作寻找游标,如果仍然失败,则只能返回失败;向左寻找的操作同向右寻找相似,具体如下:

首先取出 h PIN 标记;

使用 __bt_search 函数重置栈;将查找的记录所在的页面赋给 h

1 、如果 h 的左兄弟 prevpg 为空,转 10

2 、去除 h PIN 标记,准备回溯,初始化回溯次数 level 0

3 、使用 BT_POP 宏从栈中获取 h 的父记录 parent

4 、获取 h 的父结点页面,将其赋值给 h ,即 h 向上回溯;

5 、如果 parent 记录不为第一条索引,则将 parent 的上一条记录压入栈中;否则,去除 h PIN 标记,将回溯次数 level 递增;转 3

6 、如果 level 不等于 0 ,当前栈指向的记录为内结点记录,则将 level 递减;否则转 8

7 、获取当前栈指向的记录 bi ,将 bi 所代表的页面入站;去除 h PIN 标志,将 bi 代表的页面取出赋给 h ,即栈增长,遍历下移;将 h 的最后一条记录作为栈的当前记录,因为复键的记录如果不在统一个页面上,在必然在相邻页面的边缘处,可以参见页面分裂的策略;然后转 6 ,继续向下遍历;

8 、取出 h PIN 标记;获取 prevpg 页面赋给 h

9 、如果 h 的页面编号与游标指向的页面编号不相等,转 1 ;否则,转 10

10 、去除 h PIN 标记,将 h 页下载的缓冲池中并通过 hp 返回;

 

__bt_bdelete 函数:删除树中所有指定键值的记录;

static int   __bt_bdelete(t, key)

      BTREE *t;

      const DBT *key;

t :树指针;

key :指定的键值( DBT );

返回值:成功、失败、不确定(指定键值未找到);

伪代码

初始化已删除标志 deleted 0

1 、使用 __bt_search 函数查找键值为 key 的记录 e ,如果未找到,则根据 deleted 的值来判断返回值,如果 deleted 为真,则返回成功,否则返回失败;

2 、从 e 的位置开始向前向后删除,如果树支持复键,这样保证将所有键值为 key 的记录全部删除;需要注意的是一旦删除到页面的边缘,则需要重新查找,防止其他页面有复键的存在;具体操作如下:

3 、初始化重新查找标志 redo 0 ,将 e 所在的页面标记为 h

4 、使用 __bt_dleaf 函数删除记录 e ,需要注意的是,删除 e 之后, e 指向的记录就变成了原记录的下一条记录(如果存在的话);

5 、如果树不支持复键,则判断 h 是否为空,若是则使用 __bt_pdelete 函数删除 h ,若 h 不为空,则将 h 页面置为需写回;返回成功删除;

6 、如果树支持复键,则需要继续删除,将 deleted 1

7 、如果 e 存在且键值与 key 相等,转 4

8 、如果 e 到达 h 的边缘,则将 redo 1

9 、开始向前删除复键记录,将 e 指向的位置前移一位;如果 e 到达页面的边缘,转 14

10 、比较 e 的键值不等于 key ,转 14

11 、使用 __bt_dleaf 函数删除 e 指向的记录;

12 、如果 e 指向页面的第一条记录,将 redo 1 ,因为此时已经删除了页面的第一条记录,所以需要重新查找;

13 、转 9

14 、如果 h 是空页;使用 __bt_pdelete 函数删除 h ,转 1

15 、去除 h PIN 标记;

16 、如果 redo 为真,转 1

 

__bt_pdelete 函数:从树中删除一页;

static int  __bt_pdelete(t, h)

      BTREE *t;

      PAGE *h;

参数列表:

t :树指针;

h :待删除的叶子结点页面;

返回值:成功、失败;

伪代码

沿着栈回溯,从父结点页面中删除 h 对应的记录,如果此时导致父页面为空,则删除父页面,如此操作直至遇到父结点页面删除后不为空或到达根结点;

初始化,以 h 为当前待删除的页面;

1 、从栈中弹出当前待删除页面在父结点页面中的记录位置 parent

2 、取出当前待删除页面的父结点页面 pg ;将 parent pg 中的位置赋给 index

3 、获取当前待删除页面在父结点中的记录 bi BINTERNAL );

4 、如果 bi 存储的是大数据,则使用 __ovfl_delete 函数删除 bi 的键值;

5 、如果 pg 中只有一条记录,即当前待删除页面在 pg 中的记录,转 6 ,否则转 7

6 、如果 pg 是根结点,则将 pg lower upper 指针恢复到初始化的状态,同时将 pg 改成叶子结点页面;转 8 ;否则,使用 __bt_relink 从树中删除 pg ,然后使用 __bt_free 函数将 pg 放到空闲页面列表中,此时由于 pg 已经删除,则需要将其在父结点中的记录也删除掉,即转 1 ,迭代删除;

7 、对于删除记录后的 pg ,需要将已经删除的记录后面的记录进行前移;分为索引部分移动和数据部分移动;将 bi upper 之间的记录向高地址方向移动 bi 大小的位置,这样就将删除 bi 留下的碎片交换到了空闲区域,从而形成一个整的空闲区域,注意,此时 upper 需要同时向高地址移动 bi 大小的位置;数据的移动同样到来索引值的调整,对于存放位置地址比 bi 高的记录,其索引值无须调整,对于存放位置地址比 bi 低的记录,需要将索引值加上 bi 的大小;同时,由于 bi 的删除,在索引区域留下了保存其索引的空间碎片,同样需要将其交换到空闲区域,即将 bi 后续的记录索引前移一位,将 lower 的值减 1

8 、将 pg 设为需要写回 MPOOL_DIRTY ;转 9

9 、如果 h 是跟结点,则将 h 置为需要写回 MPOOL_DIRTY ;返回成功;

10 、使用 __bt_relink 函数将 h 从树中删除,同时使用 __bt_free 函数将 h 添加到空闲页面链表中;返回;

 

__bt_dleaf 函数:从叶子页面中删除一条记录;

int  __bt_dleaf(t, key, h, index)

      BTREE *t;

      const DBT *key;

      PAGE *h;

      u_int index;

参数列表:

t :树指针;

key :指定删除记录的键值;

h :待删除记录的页面;

index :待删除记录在页面中的索引;

返回值:成功、失败;

伪代码

首先判断游标是否指向待删除的记录,则需要调整游标;即如果 t 的游标已经初始化并且处于有效状态,指向记录的位置与待删除记录的位置相同,则使用 __bt_curdel 函数调整游标的位置;

获取待删除的记录 bl BLEAF );

如果 bl 使用了大数据,确保正确处理这些大数据;即

如果键值是大数据,使用 __ovfl_delete 函数删除键值;

如果数据值是大数据,使用 __ovfl_delete 函数删除数据值;

h index 之后的记录调整位置;

首先将数据区域中 bl upper 之间的记录向高地址移动 bl 尺寸的位置,同时调整 upper 的值,完成 bl 数据存储区遗留空间的交换;

然后调整索引的值和位置;将 index 之后的索引位置前移一位;将数据存储地址比 bl 低的记录索引值增加 bl 尺寸的大小;

如果游标在 h 页面中,并且在 index 之后,则需要调整游标的指向,将其向前移动一条记录,保证刚好指向删除 bl 前游标指向的记录;

 

__bt_curdel 函数:删除游标;

static int  __bt_curdel(t, key, h, index)

      BTREE *t;

      const DBT *key;

      PAGE *h;

      u_int index;

参数列表:

t :树指针;

key :传入待删除记录的键值( DBT );

h :待删除记录所在页面;

index :待删除记录在 h 中的索引;

返回值:成功、失败;

伪代码

所谓删除游标就是将当前游标的值(键值)保存到游标的 key 成员变量中,同时将游标置无效,即如果树不支持复键,则将游标置 CURS_ACQUIRE ;如果游标支持复键,则需要将游标置为 CURS_AFTER CURS_BEFORE ,以记录向那个方向移动可以获取与当前待删除的游标键值相同的记录,首先判断于当前游标在同一页面的情况,如果当前游标的前一条记录的键值与当前游标的键值相同,则将游标置 CURS_BEFORE ,将游标指向前一条记录;如果上述比较失败,则比较相同页面内下一条记录的键值,如果相同,将游标置 CURS_AFTER ,将游标移动到下一条记录;如果游标处于当前页面的第一条或最后一条,且当且页面内前后的记录不符合上述要求,则可以在兄弟页面查找;具体操作如下:

获取 t 的游标 c

去除 c CURS_AFTER CURS_BEFORE CURS_ACQUIRE 标记;

1 、如果 t 不支持复键,转 9

2 、如果 key 为空,则后续的操作需要比较键值,所以使用 __bt_ret 函数读取当前键值,并将其赋给 key

3 、如果 index 大于 0 ,即 c 指向的不是第一条记录,获取 c 的前一条记录 e ,比较 e 的键值与 key 的大小,若相等则将 c CURS_BEFORE ,转 8 ;否则转 4

4 、如果 index 小于 0 ,即 c 不为最后一条记录,获取 c 的下一条记录 e ,比较 e 的键值与 key 的大小,若相等则将 c CURS_AFTER ,转 8 ;否则转 5

5 、如果 index 等于 0 ,同时 h 的坐兄弟不为空,则获取 h 左兄弟最后一条记录 e ,比较比较 e 的键值与 key 的大小,若相等则将 c CURS_BEFORE ,转 7 ;否则转 6

6 、如果 c h 最后一条记录且 h 的右兄弟不为空,则获取 h 右兄弟第一条记录 e ,比较 e 的键值与 key 的大小,若相等则将 c CURS_AFTER ,转 7 ;否则去除 e 所在页面的 PIN 标记,然后转 9

7 、去除 e 所在页面的 PIN 标记;

8 、将 c 指向 e ;返回成功;

9 、使用 __bt_ret 将当前记录的键值读出存储到 c key 成员变量中备份;将 c CURS_ACQUIRE 标记;(对于只有一条记录的复键,支持此种情况)

注意: CURS_ACQUIRE CURS_AFTER CURS_BEFORE 的区别在于: CURS_ACQUIRE 表示删除游标操作后游标指向的记录是无效的;而 CURS_AFTER CURS_BEFORE 则表示删除游标操作后游标指向的记录是有效的,是与原游标键值相同的记录,根据后缀可以判断当前游标指向的记录与原游标的位置关系;

 

__bt_relink :从页面链表中删除某一个页面;

static int   __bt_relink(t, h)

      BTREE *t;

      PAGE *h;

参数列表:

t :树指针;

h :待删除的页面( PAGE );

返回值: 0

伪代码

本函数完成双向链表删除结点操作;

如果 h 的右兄弟不为空,则获取 h 的右兄弟 pg

pg 的左兄弟置为 h 的左兄弟;

pg 置为需要写回 MPOOL_DIRTY

如果 h 的左兄弟不为空,则获取 h 的左兄弟 pg

pg 的右兄弟指向 h 的右兄弟;

pg 置为需要写回 MPOOL_DIRTY

 

 

 

阅读感悟:

总的来说还是没有完全理解 BerkelyDB 的一些意图,对于阅读学习来说,还是需要记录整理才能理解;最先开始读最新的 4.8 版,发现纷繁杂乱,无从着手,所以下了个最基础的版本 1.8.6 版。第一遍通读时自以为差不多理解了程序的意图,后来准备单个文件整理下,发现还是有很多疑问,作者的意图不能理解,随着整理的推进,一些疑惑得到解决,一些疑惑又产生,最典型的就是游标的 CURS_ACQUIRE CURS_AFTER CURS_BEFORE 三个标记知道最后一个文件整理时才发现其用途。有必要在读几遍,然后在按照版本递进阅读到 4.8 版。

有一个问题一直没有搞明白,就是当大数据键值作为内节点的键值时,在删除叶子节点是不能删除这些值,但是如果内节点也删除了呢?在删除函数中未见到将大数据键值的持久存储标记去除的地方,这是否意味着即使树删除的只剩一个空的根结点,这些大数据仍然存在?

最后要说的是,其实最好的注释就是源代码!

 

你可能感兴趣的:(BI,delete,search,存储,HP,BT)