lightning mdb 源代码分析系列(3)

     本系列前两章已经描述了系统架构以及系统构建的基础内存映射,本章将详细描述lmdb的核心,外存B+Tree的操做。本文将从基本原理、内存操做方式、外存操做方式以及LMDB中的相关函数等几方面描述LMDB中关于B+Tree的使用方式。php

     介绍java

      动态查找树主要有:二叉查找树(Binary Search Tree),平衡二叉查找树(Balanced Binary Search Tree),红黑树 (Red-Black Tree ),B-tree/B+-tree/ B*-tree (B~Tree)。前三者是典型的二叉查找树结构,其查找的时间复杂度O(log2N)与树的深度相关,那么下降树的深度天然对查找效率是有所提升的;在大规模数据存储前提下,好比有100万key须要进行比较,二叉树进行查询时,须要访问的磁盘IO次数将有20次,以现有的磁盘随机访问性能,对于大型应用程序这是不可接受的性能,基础的想法就是讲多个key或者二叉树的子树存放在一块儿,以页面为单位进行访问,好比组织成下图形式,100万key比较时io访问次数只有两次,经过进一步的优化,B-Tree应运而生。node

B-Tree系列能够认为是多路平衡查找树,能有效下降树的层次以及支持外存数据组织。算法

     定义数据库

B-tree又叫平衡多路查找树。一棵m阶的B-tree (m叉树)的特性以下:数组

(其中ceil(x)是一个取上限的函数)数据结构

1)  树中每一个结点至多有m个孩子;架构

2)  除根结点和叶子结点外,其它每一个结点至少有有ceil(m / 2)个孩子;app

3)  若根结点不是叶子结点,则至少有2个孩子(特殊状况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);函数

4)  全部叶子结点都出如今同一层,叶子结点不包含任何关键字信息(能够看作是外部结点或查询失败的结点,实际上这些结点不存在,指向这些结点的指针都为null);

5)  每一个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中:

a)   Ki (i=1...n)为关键字,且关键字按顺序排序K(i-1)< Ki。

b)   Pi为指向子树根的接点,且指针P(i-1)指向子树种全部结点的关键字均小于Ki,但都大于K(i-1)。

c)   关键字的个数n必须知足: ceil(m / 2)-1 <= n <= m-1。

B-tree中的每一个结点根据实际状况能够包含大量的关键字信息和分支(固然是不能超过磁盘块的大小,根据磁盘驱动(disk drives)的不一样,通常块的大小在1k~4k左右);这样树的深度下降了,这就意味着查找一个元素只要不多结点从外存磁盘中读入内存,很快访问到要查找的数据。

为了简单,这里用少许数据构造一棵3叉树的形式。上面的图中好比根结点,其中17表示一个磁盘文件的文件名;小红方块表示这个17文件的内容在硬盘中的存储位置;p1表示指向17左子树的指针。

其结构能够简单定义为:

typedef struct {

/*文件数*/

int  file_num;

/*文件名(key)*/

char * file_name[max_file_num];

/*指向子节点的指针*/

     BTNode * BTptr[max_file_num+1];

/*文件在硬盘中的存储位置*/

     FILE_HARD_ADDR offset[max_file_num];

}BTNode;

假如每一个盘块能够正好存放一个B-tree的结点(正好存放2个文件名)。那么一个BTNode结点就表明一个盘块,而子树指针就是存放另一个盘块的地址。

模拟查找文件29的过程:

(1) 根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存。【磁盘IO操做1次】

(2) 此时内存中有两个文件名17,35和三个存储其余磁盘页面地址的数据。根据算法咱们发现17<29<35,所以咱们找到指针p2。

(3) 根据p2指针,咱们定位到磁盘块3,并将其中的信息导入内存。【磁盘IO操做2次】

(4) 此时内存中有两个文件名26,30和三个存储其余磁盘页面地址的数据。根据算法咱们发现26<29<30,所以咱们找到指针p2。

(5) 根据p2指针,咱们定位到磁盘块8,并将其中的信息导入内存。【磁盘IO操做3次】

(6) 此时内存中有两个文件名28,29。根据算法咱们查找到文件29,并定位了该文件内存的磁盘地址。

分析上面的过程,发现须要3次磁盘IO操做和3次内存查找操做。关于内存中的文件名查找,因为是一个有序表结构,能够利用折半查找提升效率。至于3次磁盘IO操做时影响整个B-tree查找效率的决定因素。

固然,若是咱们使用平衡二叉树的磁盘存储结构来进行查找,磁盘IO操做最少4次,最多5次。并且文件越多,B-tree比平衡二叉树所用的磁盘IO操做次数将越少,效率也越高。

上面仅仅介绍了对于B-tree这种结构的查找过程,还有树节点的插入与删除过程,以及相关的算法和代码的实现,将在之后的深刻学习中给出相应的实例。

上面简单介绍了利用B-tree这种结构如何访问外存磁盘中的数据的状况,下面我们经过另一个实例来对这棵B-tree的插入(insert),删除(delete)基本操做进行详细的介绍:

下面以一棵5阶B-tree实例进行讲解(以下图所示):

其知足上述条件:除根结点和叶子结点外,其它每一个结点至少有ceil(5/2)=3个孩子(至少2个关键字);固然最多5个孩子(最多4个关键字)。下图中关键字为大写字母,顺序为字母升序。

结点定义以下:

typedef struct{

int Count;         // 当前节点中关键元素数目

   ItemType Key[4];   // 存储关键字元素的数组

long Branch[5];    // 伪指针数组,(记录数目)方便判断合并和分裂的状况

} NodeType;

插入(insert)操做:插入一个元素时,首先在B-tree中是否存在,若是不存在,即在叶子结点处结束,而后在叶子结点中插入该新的元素,注意:若是叶子结点空间足够,这里须要向右移动该叶子结点中大于新插入关键字的元素,若是空间满了以至没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(固然,若是父结点空间满了,也一样须要“分裂”操做),并且当结点中关键元素向右移动了,相关的指针也须要向右移。若是在根结点插入新元素,空间满了,则进行分裂操做,这样原来的 根结点中的中间关键字元素向上移动到新的根结点中,所以致使树的高度增长一层。

我们经过一个实例来逐步讲解下。插入如下字符字母到空的5阶B-tree中:C N G A H E K Q M F W L T Z D P R X Y S,5序意味着一个结点最多有5个孩子和4个关键字,除根结点外其余结点至少有2个关键字,首先,结点空间足够,4个字母插入相同的结点中,以下图:

当我们试着插入H时,结点发现空间不够,以至将其分裂成2个结点,移动中间元素G上移到新的根结点中,在实现过程当中,我们把A和C留在当前结点中,而H和N放置新的其右邻居结点中。以下图:

当我们插入E,K,Q时,不须要任何分裂操做

插入M须要一次分裂,注意M刚好是中间关键字元素,以至向上移到父节点中

插入F,W,L,T不须要任何分裂操做

插入Z时,最右的叶子结点空间满了,须要进行分裂操做,中间元素T上移到父节点中,注意经过上移中间元素,树最终仍是保持平衡,分裂结果的结点存在2个关键字元素。

插入D时,致使最左边的叶子结点被分裂,D刚好也是中间元素,上移到父节点中,而后字母P,R,X,Y陆续插入不须要任何分裂操做。

最后,当插入S时,含有N,P,Q,R的结点须要分裂,把中间元素Q上移到父节点中,可是状况来了,父节点中空间已经满了,因此也要进行分裂,将父节点中的中间元素M上移到新造成的根结点中,注意之前在父节点中的第三个指针在修改后包括D和G节点中。这样具体插入操做的完成,下面介绍删除操做,删除操做相对于插入操做要考虑的状况多点。

删除(delete)操做:首先查找B-tree中需删除的元素,若是该元素在B-tree中存在,则将该元素在其结点中进行删除,若是删除该元素后,首先判断该元素是否有左右孩子结点,若是有,则上移孩子结点中的某相近元素到父节点中,而后是移动以后的状况;若是没有,直接删除后,移动以后的状况.。

删除元素,移动相应元素以后,若是某结点中元素数目小于ceil(m/2)-1,则须要看其某相邻兄弟结点是否丰满(结点中元素个数大于ceil(m/2)-1),若是丰满,则向父节点借一个元素来知足条件;若是其相邻兄弟都刚脱贫,即借了以后其结点数目小于ceil(m/2)-1,则该结点与其相邻的某一兄弟结点进行“合并”成一个结点,以此来知足条件。那我们经过下面实例来详细了解吧。

以上述插入操做构造的一棵5阶B-tree为例,依次删除H,T,R,E。

首先删除元素H,固然首先查找H,H在一个叶子结点中,且该叶子结点元素数目3大于最小元素数目ceil(m/2)-1=2,则操做很简单,我们只须要移动K至原来H的位置,移动L至K的位置(也就是结点中删除元素后面的元素向前移动)

下一步,删除T,由于T没有在叶子结点中,而是在中间结点中找到,我们发现他的继承者W(字母升序的下个元素),将W上移到T的位置,而后将原包含W的孩子结点中的W进行删除,这里刚好删除W后,该孩子结点中元素个数大于2,无需进行合并操做。

下一步删除R,R在叶子结点中,可是该结点中元素数目为2,删除致使只有1个元素,已经小于最小元素数目ceil(5/2)-1=2,若是其某个相邻兄弟结点中比较丰满(元素个数大于ceil(5/2)-1=2),则能够向父结点借一个元素,而后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中,在这个实例中,右相邻兄弟结点中比较丰满(3个元素大于2),因此先向父节点借一个元素W下移到该叶子结点中,代替原来S的位置,S前移;而后X在相邻右兄弟结点中上移到父结点中,最后在相邻右兄弟结点中删除X,后面元素前移。

最后一步删除E,删除后会致使不少问题,由于E所在的结点数目恰好达标,恰好知足最小元素个数(ceil(5/2)-1=2),而相邻的兄弟结点也是一样的状况,删除一个元素都不能知足条件,因此须要该节点与某相邻兄弟结点进行合并操做;首先移动父结点中的元素(该元素在两个须要合并的两个结点元素之间)下移到其子结点中,而后将这两个结点进行合并成一个结点。因此在该实例中,我们首先将父节点中的元素D下移到已经删除E而只有F的结点中,而后将含有D和F的结点和含有A,C的相邻兄弟结点进行合并成一个结点。

也许你认为这样删除操做已经结束了,其实否则,在看看上图,对于这种特殊状况,你当即会发现父节点只包含一个元素G,没达标,这是不可以接受的。若是这个问题结点的相邻兄弟比较丰满,则能够向父结点借一个元素。假设这时右兄弟结点(含有Q,X)有一个以上的元素(Q右边还有元素),而后我们将M下移到元素不多的子结点中,将Q上移到M的位置,这时,Q的左子树将变成M的右子树,也就是含有N,P结点被依附在M的右指针上。因此在这个实例中,我们没有办法去借一个元素,只能与兄弟结点进行合并成一个结点,而根结点中的惟一元素M下移到子结点,这样,树的高度减小一层。

为了进一步详细讨论删除的状况。再举另一个实例:

这里是一棵不一样的5阶B-tree,那我们试着删除C

因而将删除元素C的右子结点中的D元素上移到C的位置,可是出现上移元素后,只有一个元素的结点的状况。

又由于含有E的结点,其相邻兄弟结点才刚脱贫(最少元素个数为2),不可能向父节点借元素,因此只能进行合并操做,因而这里将含有A,B的左兄弟结点和含有E的结点进行合并成一个结点。

这样又出现只含有一个元素F结点的状况,这时,其相邻的兄弟结点是丰满的(元素个数为3>最小元素个数2),这样就能够想父结点借元素了,把父结点中的J下移到该结点中,相应的若是结点中J后有元素则前移,而后相邻兄弟结点中的第一个元素(或者最后一个元素)上移到父节点中,后面的元素(或者前面的元素)前移(或者后移);注意含有K,L的结点之前依附在M的左边,如今变为依附在J的右边。这样每一个结点都知足B-tree结构性质。

B+-tree:是应文件系统所需而产生的一种B-tree的变形树。

一棵m阶的B+-tree和m阶的B-tree的差别在于:       

1.有n棵子树的结点中含有n个关键字; (B-tree是n棵子树有n-1个关键字)

2.全部的叶子结点中包含了所有关键字的信息,及指向含有这些关键字记录的指针,且叶子结点自己依关键字的大小自小而大的顺序连接。 (B-tree的叶子节点并无包括所有须要查找的信息)

3.全部的非终端结点能够当作是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (B-tree的非终节点也包含须要查找的有效信息)

a)      为何说B+树比B-tree更适合实际应用中操做系统的文件索引和数据库索引?

1) B+-tree的磁盘读写代价更低

B+-tree的内部结点并无指向关键字具体信息的指针。所以其内部结点相对B-tree更小。若是把全部同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的须要查找的关键字也就越多。相对来讲IO读写次数也就下降了。

举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点须要2个盘快。而B+-tree内部结点只须要1个盘快。当须要把内部结点读入内存中的时候,B-tree就比B+-tree多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。

2) B+-tree的查询效率更加稳定

因为非终结点并非最终指向文件内容的结点,而只是叶子结点中关键字的索引。因此任何关键字的查找必须走一条从根结点到叶子结点的路。全部关键字查询的路径长度相同,致使每个数据的查询效率至关。

   关于B-Tree/B+-Tree的相关代码能够见后续参考。后续我的也会再发系列博文详述。

     外存操做

     前面的理论性介绍都是从别的地方摘抄过来的,详细描述了B-Tree在内存中的操做步骤以及须要考虑的各类情形以保证B-Tree的结构。从前文中描述可知,此种数据结构是很适合进行持久化保存的,其主要缘由就是,每次数据的变化(增长、删除)致使的树的形状变化时影响的节点比较小,这样在存放到外存时,就不须要更改整个B-Tree,只需修改被影响的节点,从而大大减小了IO次数。

      在真正的系统中(数据库和文件系统),使用B-Tree存储时,不会使用以上相似方式存储数据,而是采起页的方式管理,页式内存管理已经在操做系统实现中被证实是一种成熟高效的管理方式。在其余应用系统中使用页管理数据文件,有以下几大优势:

     1. 批量化IO操做,避免没必要要的IO操做

         经过将相邻的IO操做归一化到同一个页面中进行操做,能够避免屡次IO,若操做系统比较繁忙,且同一页面的随机IO请求次数较多但时间比较分散时,此时效率最低,系统将不得不每次进行寻道。归一化到以页面为单位,则能够避免相似情形。

     2. 能尽可能使用操做系统优点

       操做系统一样以页为单位,若将B-Tree数据管理的页面大小设置成系统页面大小,在进行IO操做时、内存操做时,OS将能以最优方式进行存储、传输,从而提高性能。

     3. 让B-Tree进入实战阶段

       根据前文描述,B-Tree须要预先定义阶数,好比5阶,7阶等,若实际状况采起相似方式定义,则须要预先评估应用须要存储的数据个数,这对于相似于数据库系统、文件系统是不可能任务。以页为单位,则用近似的方案避免了此问题,在真正数据库系统中,采起将索引存储到一个页面当中,一个页面至关于一个节点,一个页面节点满了再进行数据插入时则进行页面分裂。这样既避免了须要预先定义阶数,又避免了因不一样节点数据大小不一致致使的内存大小不一致问题。为何使用页面而不定义阶数,使用页面的管理方式可以保持B-Tree结构和性质呢?

    LMDB使用

    下面讲述LMDB中如何实现B-Tree结构的?

     LMDB中代码主要分为两块以实现B-Tree结构。

     1. page管理

        page管理实现了上述描述的外存操做方式,具体实现的功能包括

  • 新建、分配、释放页面(mdb_page_new,mdb_page_alloc ,mdb_page_mallocmdb_page_free )

          mdb_page_malloc:为新页面分配内存

          从操做系统中申请1个或者n个页面,通常为一个页面,n个页面为overflow页面,分默认会在分配时将初始化最后一个页面为0。

         mdb_page_free:释放单个页面,将它放入可重用页面列表。

        mdb_page_new::新建页面

         首先调用mdb_page_alloc分配页面,而后初始化页面,新建一个页面时,认为这个页面是一个全新的页面,所以须要其整个空间可用,初始化设置将体现这点。

        mdb_page_alloc: 分配页面

        分配一个或n个页面,若分配n个,则n个页面是连续页面。若事务中可用脏空间没有了,则分配失败,可用脏空间是指存储脏页ID的数组大小,配置为131071。LMDB中全部可用的脏页一样被维护成一颗B-Tree,freeDB中记录了最后一次放入页面的事务ID,每次分配时都从freedb中寻找足够大重用空间,通常分配一个页面能知足,连续页面,可能须要尝试屡次,所以多个页面通常是overflow 页面,必须是连续页面才能知足要求。FreeDB的构建过程以及存储格式见本系列其余博文。

  • 复制页面(mdb_page_copy )

         将页面内容从一个页面复制到另一个页面,此功能主要用于cow

  • 页面分裂合并(mdb_page_merge ,mdb_page_split )

        页面的分裂合并是用于B-Tree插入删除时,为知足平衡子节点须要进行的操做。具体应用条件参见本文前面关于插入、删除的描述。

       页面分裂:

        mdb_page_split:实现了上述B-Tree的操做过程,考虑了仅有一个节点时、append模式、braches/leaf/leaf2等不一样页面的处理过程,基本流程就是根据必定的算法肯定分裂点,根据B-Tree的定义,在分裂时,不必定须要保证平分,只须要保证页面节点保持半满便可。分裂点肯定以后,就进行数据的移动并插入致使分裂的数据以及修改指针以维持B-Tree结构,同时再决定是否会致使上层分裂以及root分裂,若会则进行递归处理。

      页面合并:

      mdb_page_merge:一样是实现了上述由于节点删除致使的merge过程。基本过程是,将合并的目标页面置为脏页,而后根据上述理论状况进行节点的一个个复制,或者对于内部节点而言进行页面指针调整以及进行上下节点的移动,对于本页完成以后进行平衡操做,其中平衡操做可能会又致使merge操做,直到B-Tree从新知足定义为止。

  • 脏页读出写入(mdb_page_spill,mdb_page_unspill,mdb_page_dirty,mdb_page_flush,mdb_page_touch )

          mdb_page_spill:将脏页写回磁盘,这是为了嵌套长事务进行的设计,有些嵌套长事务会使用大量的页,为了不耗光内存,能够将脏页写回磁盘,写回磁盘如同commit同样,由于多个进程、线程之间将只会存在一个写事务,所以在未提交之间前写回磁盘没有任何问题。并且只要能有空间,页面就不会刷入磁盘。在执行时,先计算是否空间足够,不够的将id存入idl数组,而后刷入磁盘,再根据环境变量决定是否保留p_dirty标记。

       mdb_page_unspill:将spill的页面从新读回,这就不须要进行touch,直接设置dirty标志就能够了。lmdb支持嵌套事务,所以在查找页面是否属于已经被spilled的页面须要查找整个嵌套路径,从叶子到跟,找到以后确认midl列表(脏空间)是否有足够空间,没有的提示事务空间已满,不然加载页面并设置脏页标记。

       mdb_page_dirty:设置脏页标记,并将脏页加入到事务中的脏页列表当中。

       mdb_page_flush:用在事务提交时,当清除页面脏页标记后,将数据更新到磁盘(经过写文件方式).若使用试验的特征(mmap写),则在清除脏页就完成工做,由于写操做交给系统完成,不然须要计算文件起止地址后将页面一页页的写入磁盘,最后释放脏页内存,特殊缘由须要留存内存的页面不参与flush。

       mdb_page_touch: 实现COW的技术,复制一个页面,并将更新过B-Tree指针关系的页面插入到B-Tree当中,这样意味着在修改时是在复制的页面上进行修改,别的事务在本事务没有提交以前看到仍是之前的数据,提交以后的新事物看到的才是修改以后的数据。

  • 页面查找(mdb_page_search_root ,mdb_page_search ,mdb_page_search_lowest)

          mdb_page_search_root:从B-Tree根节点检索,根据key的值,从根节点开始遍历子树获取每一层对应的page,在page以内检索key,再根据B-Tree查找方法肯定下一层子节点的page,层层遍历,从而最终肯定key的位置或者判断B-Tree中没有对应的key。同时将页面存放到cursor页堆栈中。这样cursor将能够重用对应的页面,为后续进行更新等操做提供便利。

        mdb_page_search/mdb_page_search_lowest都将调用mdb_page_search_root以完成检索

        mdb_page_search,除了完成检索为的附加工做是确保所使用的B-Tree在本事务可见范围内是最新版本,同时在须要时将页面置为脏页。

        mdb_page_search_lowest: 从当前分支页开始,检索第一个符合条件的值。

  • 页面获取(mdb_page_get,mdb_page_list)

       mdb_page_get:获取页面,原本根据MMAP原则,读取对应的页面很是简单,计算下地址便可,但lmdb中,考虑到事务可能使用大量的页面,事务可用空间满时,将一部分页面spill/flush到磁盘中,所以须要在get时判断是否在spill列表中,在的话从中获取,不然直接计算获取。

      mdb_page_list:显示页面中的全部key,是个工具方法。

      2. cursor操做

        cursor操做实现了B-Tree节点操做,cursor指向当前须要进行操做的B-Tree节点,而后依据提供的操做方式(insert、del)进行数据操做,而后进行一系列复杂的操做流程以维持B-Tree结构。具体实现的功能包括:

  • 游标遍历(mdb_cursor_sibling,mdb_cursor_next,mdb_cursor_prev,mdb_cursor_first
    ,mdb_cursor_last )

        mdb_cursor_first: 将游标定位至B-Tree的最小叶子节点(第一个),而非根据key查询时获得第一个结果位置。若支持重复数据,还要特殊处理,移动到重复数据第一个。

        mdb_cursor_last:与first相似,只不过定位至最大叶子节点(最后一个)

        mdb_cursor_next: 游标移动至下一个节点

        mdb_cursor_prev: 游标移动至前一个节点

        mdb_cursor_sibling:将游标移动至兄弟节点,能够是前一个页面或者下一个页面。

若当前页有key,则行为与next、prev相似,不然移动到下一个页面的对应key位置。

  • 增删改查(mdb_cursor_get ,mdb_cursor_set ,mdb_cursor_del ,mdb_cursor_del0 ,mdb_cursor_put ,mdb_cursor_count )

          mdb_cursor_get:根据游标位置和条件获取值,最经常使用:MDB_GET_CURRENT,获取游标所指节点的值,基本思路是看页面中索引是否已经大于key个数,大于则说明游标已经须要指向下一页,对于取当前值的不重复key来讲,这不可能,所以获取失败。而后根据是否为leaf2页面(key重复),是根据宏取值,不然判断叶子值是否有副本(彻底重复key和值,有的话初始化xcursor并开始取值,不然话直接读取对应位置的值。

       mdb_cursor_set :将游标设置(定位)到指定key位置,假如已经在正确页面,只须要判断key是否在页面key的范围以内,判断最大、最小值能够肯定。而后根据相应标志,如同get中所说,进行判断以及读取或设置某些变量。不然话进行页面查找先定位key所在页面(mdb_page_search),而后定位页面中位置(mdb_node_search),而后再设置相关变量。

      mdb_cursor_count:返回游标表明的结果数,惟一key返回一,重复key返回重复个数。

      mdb_cursor_put:将key、value对存放到数据库中,默认是新增长,若key已经存在则是更新,基本流程是:判断前提cursor、key非空,确认各类标志是否合法,好比多个value,可是数据库不支持重复key这种情形就不合法,标志合适以后,判断是否为空树,非空时将cursor指向正确的位置,好比append模式指向数据库最大节点以后,正常指向应该插入的位置。而后touch全部页面使全部页面可写。若为leaf2类型页面,说明key、value彻底重复,增长key就OK了,而后再判断value值是否太大,太大则转换为子树进行存储。转化为subdb/subpage时,首先根据各类标志设置各类变量,包括申请新页等,而后其他的就是根据各类标志完成上述理论描述的节点插入动做,将值放置对应位置、进行分页等,须要时进行unspill,放置到overflow页面等,若一次插入多条数据还须要屡次重复进行一次一条的插入。

      mdb_cursor_del,mdb_cursor_del0:删除指定key、value。首先是根据各类标志设置各类变量,其次设置页面为脏页,其次若删除以后,subdb/subpage,overflowpage等收到影响,则须要将对应页面回收到free-list,好比subdb删除最后一个节点时,须要删除整棵子树。真正的key删除在del0中,它从页面中删除对应的key,删除完成后对整个B-Tree进行rebalance,而后修正全部指向当前删除页的同一事务内的其余cursor,通知其余cursor此页面已经被删除。

  • 打开、关闭、重用、初始化
    mdb_cursor_touch:将数据库以及在cursor堆栈中的全部页面设置为脏页。这样可能会有少许页面实际不须要设置为脏页实际设置为脏页的情形,但这样为实现COW提供最大的便利,只须要修改root页面指针便可,不然须要跟踪不少页面。
    mdb_cursor_open:打开游标,首先判断标志是否合法,合法就申请内存并调用init初始化
    mdb_cursor_renew:重用游标,当本游标已经再也不使用,能够renew重用。
    mdb_cursor_close:关闭游标,从事务的cursor列表中删除,释放内存。

          mdb_cursor_copy:复制游标,将全部内容从一个复制到新游标。

          mdb_cursor_shadow:备份cursor对应事务的游标,用于envcopy

          mdb_cursor_init:设置各类变量,若数据库状态为DB_STALE,则需获取最新的root节点。

  • 页面
           mdb_cursor_pop:从cursor堆栈中弹出一个页面
          mdb_cursor_push: 将一个页面压入堆栈,通常会将整个search路径上的全部页面压入堆栈。
  • 状态
    mdb_cursor_chk:检查cursor是否正确
    mdb_cursor_txn: 获取cursor对应事务
    mdb_cursor_dbi:获取cursor对应数据库

以上内容简单介绍了lmdb内部使用的B-Tree原理,以及针对B-Tree操做的各类函数的简单介绍,关于B-Tree存储subdb,subpage,overflowpage等还未足够仔细,如有时间将在后续博文中单开一篇详细描述以上非正常key-value的数据存储和操做方式。LMDB中的B-Tree实现的是B+-Tree,全部叶子节点都在同一层级。

      本文中不足之处不少,欢迎交流指正。     

参考文献:

https://en.wikipedia.org/wiki/B-tree

http://blog.csdn.net/hbhhww/article/details/8206846

http://slady.net/java/bt/view.php

计算机程序和艺术