H2的存儲子系統——MvStore

  MvStore是多版本的,持久化的,以LSF爲寫入策略的的Key-Value存儲系統,是作爲H2的新一代存儲子系統設計,在H2的架構之中處於第二層,即在文件抽象層之上。它的特點如下:

  • 基於多版本頁數據結構(包括B樹和R樹實現)
  • java.util.Map爲基礎Key-Value存取接口
  • 多存儲形式支持(內存、普通文件、加密文件、壓縮文件)
  • 事務與併發讀寫支持

  下面以官方的例子來看看MvStore的基本用法,官方網頁爲http://www.h2database.com/html/mvstore.html

import org.h2.mvstore.*;

// open the store (in-memory if fileName is null)
MVStore s = MVStore.open(fileName);

// create/get the map named "data"
MVMap<Integer, String> map = s.openMap("data");

// add and read some data
map.put(1, "Hello World");
System.out.println(map.get(1));

// close the store (this will persist changes)
s.close();

  在這裏我們看到,一個MvStore關聯一個文件或者是內存,然後從MvStore中可以建立或者打開一個MvMap,這是個實現了java.util.Map 接口的類,然後就可以通過接口的put和get來進行key-value操作了。
  這個MvMap很容易引起兩個問題。
1. MvMap與JDK中的HashMap或TreeMap的差別
2. MvMap及MvStore與H2這樣一個關係型數據庫的關係
   回答第一個問題很容易,當store close時,我們可以看到,MvStore打開的fileName文件(fileName不爲Null的話)不再是0kb而是有了12kb的大小。說明,MvMap是一個持久化的Map,而不是和JDK中HashMap和TreeMap那樣是一個內存中的Map。
  回答第二個問題則得向上層看。有過關係型數據庫使用經驗的朋友都知道,數據庫裏面最主要的單位是表,也叫關係表,這個表格有行有列,每一行有一個主鍵,是作爲行的唯一代表。在H2中以MvStore爲磚石搭建的世界裏,這樣的對象就是MvTable,每個Table至少有一個MvPrimarylIndex,而MvPrimarylIndex最主要的組成部分是一個TransactionMap的dataMap,而TransactionMap就是包裝了事務功能的MvMap,這也如H2架構篇博客分析的那樣的層次Table/index->Transaction->Store。這個Map的Key是主鍵,Value則是行。
   讀懂這個兩個問題可以說就能讀懂數據庫中(沒錯是數據庫而不僅僅是H2數據庫)的很多問題。無論SQL或者是NoSQL,基本上數據庫的最底層都是這麼個類似的Key-Value系統,不同是Key是什麼,Value又是什麼。
  關係數據庫在Key-Value的基礎上以主鍵爲Key,以行爲Value構建了表,列存儲數據庫如HBase將這些行打散,成爲用rowkey相關聯的單獨列單元,文檔數據庫如MongoDB則是變value爲文檔,Redis則是乾脆剝乾淨外衣,變成了純粹的Key-Value系統。
   同樣,不同的還有怎麼去寫和讀這些Key-Value。事實上,也就是怎麼將這些數據精細地轉化於各個存儲介質之間,可以只存於內存,比如Redis或H2、sqlite、Mysql的內存數據庫形式,也可以是單文件如H2、sqlite的文件形式,還可以是多文件而統一於表空間,如Oracle、Mysql,postgresql等,甚至可以是分佈式文件系統。
  H2提供了幾種方式,內存方式、單文件方式、寄存方式(存儲於postsql),貌似還有集羣形式(還待驗證是什麼樣的集羣)。其中,單文件的方式是用的最多的一種方式,也是我們關注的重點。
   正如H2架構文章所分析的那樣,Store一層的結構關鍵詞可以歸納爲兩個:B-tree和Page,當到了MvStore上時,可以加上一個Chunk。首先說說B-tree,分析B-tree的文章已經很多了,它的出現是我們在主存和輔存之間做出妥協的結果。如果是數據全在內存裏,一顆AVL樹或者紅黑樹是最合適的樹狀查找結構。但當我們數據很多,不足以全部載入內存時候,就比較尷尬了,如果二叉樹全存在磁盤上,那每訪問一個節點就去讀磁盤,這是無法忍受的。那麼一次讀一塊呢?這就有了Page的概念,在操作系統中主存和輔存也是這麼交互的。操作系統裏的Page(頁)是固定大小的內存,一般爲4k,讀寫數據時,哪怕你讀一個字節或寫一個字節,載入主存或寫入輔存的也是一個頁。而作爲數據庫,要最大限度的減少磁盤讀寫,就是儘量把這些頁塞滿,這樣,讀一次就有最多有用的數據,因此數據庫的Page往往設置與操作系統的Page相同或者是操作系統Page的整數倍。把這些Page以類似目錄的形式連接,這就有了B-tree,它在樹裝存儲結構和順序表之間做了一個平衡,簡單的說,B樹就是個多級目錄,就如我們寫文章時,一級標題1,2,3….,二級1.1,1.2,1.3….,三級1.1.1,1.1.2,1.1.3….一樣,到了最後纔是數據。而這些個標題每一級是順序存儲的,如1,2,3….是一塊,1.1,1.2,1.3….是一塊,1.1.1,1.1.2,1.1.3….是一塊。這些塊之間以指針相連接,這些塊就是B樹節點。關於B樹更多內容可以看百度百科-B樹
  但是,B樹裏面存的數據是什麼是不一定的,在課本里經常提到的是B樹的階數,一般用m代替。並且說這個m一般由頁的大小確定,比如頁大小爲4096 Byte,裏面存的是int一個是4 Byte,那麼就用1024階,即一個節點存1024個索引項或數據項。但是如果索引項和數據項不固定的呢?H2在這裏對Page做了一定修改,它裏面的性質如下:

  • 一個Page至少有一個數據或索引項
  • Page裏的數據項數量不定

  這樣Page的大小是一個參考值而不是限定值。也就是說,如果一個Page裏只有一條數據,而那個數據特別大,這條數據不能分在兩個Page,這時Page只能適應數據的大小。而Page裏面的數據數量是不一定的,只要它儘量填滿頁的空間。而在H2中,每個Page就是一個節點,因此,H2中的B樹實際上是隻能說是一個非典型的B樹。與此相對的是Sqlite,Sqlite的頁大小是一致的, 因此,它的葉子節點也分爲空閒頁、普通頁和溢出頁,頁內還有碎片、自由塊。這無疑是一種更精細的空間控制,也更加複雜,這將在以後分析。
   Chunk則是MvStore最大的特點,這也可以從它的名字看出來。MvStore全稱是Muti-Version Store。它的存儲是這樣工作的。

    MVStore s = MVStore.open(fileName);
    MVMap<Integer, String> map = s.openMap("data");
    for (int i = 0; i < 400; i++) {
        map.put(i, "Hello");
    }
    s.commit();
    for (int i = 0; i < 100; i++) {
        map.put(i, "Hi");
    }
    s.commit();
    s.close();

這樣Commit了兩次,會出現兩個Chunk
Chunk 1:
- Page 1: (root) 兩個指針指向 page 2 和 3
- Page 2: leaf 有140個數據 (keys 0 - 139)
- Page 3: leaf 有260個數據 (keys 140 - 399)
Chunk 2:
- Page 4: (root) 兩個指針指向 page 3和 5
- Page 5: leaf 有140個數據 (keys 0 - 139)
  第二次修改,Upate了Page2之中的前100個數據,它不是直接修改Page1,而是copy了Page1產生了Page5。這樣的好處是什麼呢?官網的解釋是可以增加併發讀寫,但是沒有給出詳細理由。於是這隻能自己琢磨,假設有多人同時修改數據,如果不用多版本,那就必須使用鎖,有鎖就有先來後到,這無疑是降低併發性的。而用多版本,則在很大程度上解決了這個問題。併發的讀寫可能會在不同版本進行,由此可以進行無鎖讀寫。
   到這裏,B-tree,Page,Chunk已經大致說清楚了,剩下最後一個問題,這裏的Key和Value是啥。H2裏的Key和Value都是Value,存在org.h2.value 裏。每一種Value繼承自Value抽象類,比如Long類型,就是爲ValueLong等等。總的包括int,short,long,double,float,string,decimal,blob,datetime多種類型。基本類型的組合是ValueArray來統一各種Value。ValueArray是row的實際值。另外,由於java沒有sizeof運算符,對每種類型還得分別把其實際大小寫入Value類中,從而使得數據庫使用的內存可以計算和控制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章