H2的存储子系统——MvStore

来源:互联网 发布:c语言产生0 9的随机数 编辑:程序博客网 时间:2024/05/01 17:22

  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 datamap.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类中,从而使得数据库使用的内存可以计算和控制。

0 0