在這一篇里,我將用圖文的方式展示LiteDB中頁的結構及作用,內容都是原創,在描述的過程中有不准確的地方煩請指出。
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.后面的話
這章博客寫完,我才知道想把一件事情說明白真是不容易,況且是比較復雜的源碼。相信我會堅持更新,也希望能與看我博客的各位大神多多交流。