LiteDB原始碼解析系列(2)資料庫頁詳解

程式設計師小張發表於2019-06-14

 1.LiteDB頁的技術工作原理

  LiteDB雖然是單個檔案型別的資料庫,但是資料庫有很多資訊,例如索引,集合,檔案等。為了管理這些資訊,LiteDB實現了資料庫頁的概念。頁是一個擁有4096 位元組的 儲存相同資訊的地址塊。頁也是操作磁碟檔案(讀寫)的最小單元。LiteDB有6中頁型別,類圖如下:

1.1 BasePage

  BasePage是資料庫頁型別的父類,使用一個常量欄位PAGE_SIZE定義了頁的大小為4096個位元組。

  主要的屬性說明如下:

  PageID:一個uint型別的ID,在LiteDB資料庫中,不管是哪種頁型別,這個PageID都是不一樣的。

  PrevPageID:指向上一頁的ID。如果指向一個uint的最大值(4294967295)即表示沒有上一頁。

  NextPageID:指向下一頁的ID。如果指向一個uint的最大值(4294967295)即表示沒有頁一頁。從這裡看頁的結構有點像雙向連結串列,但在實際儲存中,頁與頁之間貌似並不是以連結串列形式儲存(可能我這裡也沒太搞明白這兩個ID的具體作用)。

  PageType:一個頁型別的列舉,根據這個列舉,可以將BasePage強制轉換成相對應的頁型別。

  ItemCount:一個用於計算頁中的特定資料大小的ItemCount,在DataPage和IndexPage中可以看到它的作用。

  IsDirty:從名稱我們可以大概猜出它的作用,在LiteDB中專門用來標記該頁的資料是否完成Commit操作。

  FreeBytes:這個屬性需要各個子類重寫,用於計算該頁還有多少可用的位元組。

1.2 CollectionPage

  一個CollectionPage就代表一張表,比如資料庫中有Customer和Order兩張表,那麼在資料庫中就存在兩個CollectionPage分別儲存這兩張表的資訊。CollectionPage之間用 Prev/Next 連線。

  CollectionPage中有一個CollectionName代表了該表的名稱,比如Customer和Order兩張表就有兩個名稱分別為“Customer“和“Order”。DocumentCount屬性標誌著該表有多少條記錄,比如向Customer表插入100條資料,那麼DocumentCount就為100。

  CollectionPage裡面最主要的一個屬性Indexes,這是一個自定義結構體CollectionIndex的陣列,它代表了該表中的所有索引名稱。比如Customer表中有三個欄位分別“ID”、“Name”、“Age”,(特別強調,這三個欄位都要作為索引)那麼Indexes陣列就分別包含了這三個欄位。同時可以看到這個頁中有個叫PK的屬性,根據名稱我們大致就知道它肯定是主鍵,比如上面Customer表中的“ID”。

1.3 HeaderPage

  HeaderPage儲存了當前使用的LiteDB資料庫的一些資訊,包括標頭檔案,資料庫版本,可用的空餘頁ID,使用者版本等。其中有個名為ChangeID的屬性,這個是用於處理事務時,驗證客戶端中的ChangeID是否一致。

1.4 DataPage

  DataPage就是資料頁,它的結構最簡單,除了父類之外只包含一個名為DataBlocks的字典,這個字典Key是一個ushort數字,Value是一個DataBlock類。後面我們通過分析DataBlock這個結構體就大致能知道DataPage的作用。同時在資料頁中還可以看到它重寫FreeBytes屬性就是可用位元組減去字典長度,ItemCount就等於字典長度。

1.5 ExtendPage

  ExtendPage是資料擴充套件頁,如果插入的資料過大時,就講超過Page的資料塊放入ExtendPage中的Data中,同樣它重寫FreeBytes屬性就是可用位元組減去Data的長度。

1.6 IndexPage

  IndexPage就是索引頁,它的結構和DataPage類似,只包含了一個名為Nodes的字典,這個字典中key是一個ushort數字,Value是一個IndexNode類,索引使用跳錶的形式進行儲存。

2. 資料視覺化——掀開Page的面紗

  可能看完上面說明,你可能對資料頁仍然是一頭霧水,我在看原始碼的時候也是如此。後來經過我各種努力,想出了 一個辦法,就是將頁的資訊實時展示出來,也就是常說的資料視覺化。後面我會專門介紹如何把資料頁的資訊展示出來,這裡大家先跟著往下看。

  首先我們先建立一個資料庫:

LiteEngine db = new LiteEngine(Path);

  下面我用winfrom做的介面,Header下面的列表就表示所有的HeaderPage資訊,其他也類似。由於ExtendPage功能比較簡單,所以沒把ExtendPage的資訊展示出來。在執行完這條命令後,介面中就可以看到有兩個頁被建立了:

      我們可以看到HeaderPage和CollectionPage各建立一頁,HeaderPage因為是要存放資料庫的資訊,所以在資料庫一被建立就有且只有一頁,後面再新增新的表,HeadPage也只有這一頁。CollectionPage為什麼會有一條呢,看它的表名稱,你就知道了,master表,是不是很熟悉?沒錯,和Sqlserver資料庫的系統資料庫類似,只不過我確實看不出來LiteDB裡面的這張master表作用。

  然後我們在新增一張名為customer的表,欄位分別是ID,Age,Name,Address。即執行下面一段話:

var col = db.GetCollection<Customer>("customer");
col.EnsureIndex("Age");//確定Age欄位為索引
col.EnsureIndex("Name");//確定Name欄位為索引 

  執行完之後,我們可以看見有CollectionPage中有一個新表被建立,它的表名為customer,這個page中有三個表索引,分別就是預設的ID主鍵,然後是Age和Name,注意欄位Address並沒有新增進去。

  IndexPage增加了三頁,每頁對應一個索引,同時頁裡面只有一個索引節點(IndexNode)。

  DataPage資料頁目前還是空的。

  我們再插入3條記錄,執行語句如下:

for (int i = 0; i < 3; i++)
{
    var customer = new Customer
    {
       Id = i,
       Name=i%2==1?"Jim1_"+i.ToString():"Jim2_"+i.ToString(),
       Age = i*10,
       Address = "Dump"
    }
     col.Insert(customer);
} 

  然後我們就能夠看到每個索引頁多了三個索引節點,同時資料頁也建立了一頁。最後我們再新增兩張表,一張Order表欄位為ID,Ordersum。另一張Product表為Id,ProductName和ProductPrice。兩張表的欄位全都設成索引,資料頁結構如下:

  

 

   從上面的可以清楚看到,每新增一張表,就會建立一個CollectionPage頁,向表裡新增資料的同時,如果有索引新增,那麼IndexPage頁裡相應內容也會新增。把上面圖中資料再詳細繪製出來就是下面這個結構:

  從這張圖,大家應該很容易就看懂這幾種頁型別的作用。注意當前0.0.8版本的ID要指定為BsonID,在內部會改為"_id",這也是這個版本在某些地方出bug的原因。同時DataPage裡面存的資料並不是圖上面的json格式而是Byte陣列。

3.後面的話

  這章部落格寫完,我才知道想把一件事情說明白真是不容易,況且是比較複雜的原始碼。相信我會堅持更新,也希望能與看我部落格的各位大神多多交流。

相關文章