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

計算機程序和藝術

相關文章
相關標籤/搜索