吵吵   2019-03-02  阅读:1,407

上次我们说到自从用了NoSQL数据库之后,感觉编程效率极大的提高了,抛弃了对象和关系的映射ORM,整个人都清爽很多了。

但是要用好NoSQL数据库的话,我们还是得了解它的基本原理,设计思路,才能在编程路上避免很多的坑。

LiteDB是一个mini的单文档的本地数据库,且是仿照大名鼎鼎的MongoDB设计思路来的,编写的语言是C#,读起来也很方便。我们要研究NoSQL数据库,那么把LiteDB的文档和代码读一遍是个很好的选择。

我们带着问题来读。

一、类是以什么格式来存储的?

以简单的编程角度来看,如果我们要存储一个类,我们大概会先把类的字段拿出来,一个个字段存储按照顺序排列好,比如

Class A
{
FieldA
FieldB

}

序化之后大概是 FieldA^FieldB^… ^代表的是分割符,并不是没有数据库会这样去做,但是如果位置固定了FieldA是第一位,那么字段就只能增加不能修改了!

对于一个类来说,尤其是复杂的类,结构上属于树形的数据结构,因此如果对应存储的文档格式,树形结构的xml,json等格式才是刚好一一对应的。但是LiteDB并没有采取通用的JSon格式来存储,而是用了一种MongoDB使用的BSon的数据格式。

二、BSon和Json之间有什么不同?

BSon和JSon最大的区别就是Bson是用字节即二进制的格式来存储数据的,而Json是纯文本。

一个BSon文档最简单的格式是:

document ::= int32 e_list “\x00”

即用一个int32来标记了文档的长度,用”\x00″来标记了文档的结尾。这么做的一个最重要的优势就是BSon读起来的速度会非常快,因为我只需要先读到int32这个代表文档长度的数据,然后就可以直接越过这个文档去读取下一个文档。但是Json是不行的,Json是需要把所有的文档全部都加载进来,你才能知道它具体是个什么样子的结构,这个在读起来就相当的麻烦了,尤其碰上大一点JSon文档,解析简直噩梦。

三、如果要修改一个类怎么办?

由上述内容我们知道,LiteDB用来是把类序化成BSon文档格式来进行存储的,一个类对应的就是一个Bson Document。因此MongoDB也叫文档数据库,一个类对应就是一个文档。

因此,类你是想怎么修改都可以的,只要类名不变,最终都会序化成Bson Document存储,你可以不断增加字段,最多就是这个文档变大一些罢了。

但是更改字段名后会有一些问题,比如你把FieldA改成了FiledA1,但是所对应的数据还是不变,那么以前存储的类你都要取出来,把FieldA的值赋予FiledA1,再Update一遍,否则当你下次Update这个类,这个类序化成Bson Document的时候,FieldA对应的数据就丢失了。

四、LiteDB是如何在磁盘或者内存中存储这些BsonDocument?

虽然我们已经把类序化成了二进制的数据或者说叫文档,但是这些文档如何存储,如何快速读取又通过什么样的机制来解决呢?

首先是Page的概念,我们在源代码的Engine里面的BasePage.cs找到这些代码:

public enum PageType { Empty = 0, Header = 1, Collection = 2, Index = 3, Data = 4, Extend = 5 }

public const int PAGE_SIZE = 4096;

///

/// This size is used bytes in header pages 17 bytes (+8 reserved to future use) = 25 bytes
///

public const int PAGE_HEADER_SIZE = 25;

///

/// Bytes available to store data removing page header size – 4071 bytes
///

public const int PAGE_AVAILABLE_BYTES = PAGE_SIZE – PAGE_HEADER_SIZE;

类似于内存的分页机制,LiteDB把磁盘或者内存中最小的读取和写入单元叫做Page,每个Page的大小是4096字节,因此也叫4K数据库。定义好了Page之后,磁盘的读写就非常简单了,每次读4096个字节,然后不断读下去,或者你跳着读写也行,反正是4096的整数倍。

五、Page是如何分类的?

一共分成了6种类别:

Header Page: 这个叫头部的Page,不用用想也知道就是保存了数据的一些基本信息了,比如版本啊,数据库名称呀等等。这个Page或者说4096 byte的数据块就是数据库文件的第一个Page了,也就是说你建立了一个新数据库什么都没有的话,还有4096 byte,保存的内容就是Header Page。

Collection Page: 说到这个,得先搞清楚一个概念,SQL数据库中,我们一般把类名对应成数据库的表名,一个类对应一个表。而在NoSQL中,一个类对应的是一个集合,即Collection。那么Collection Page也就好理解了。一个Collection Page对应到一个类的描述,记住不是数据,只是描述。比如类的名称啊,哪些字段是索引呀,以及其它一些信息。上面说到一个Page就4096 byte大,这个Collection Page还要存储哪些字段是索引字段,长度不够呀,所以一个类能加索引的字段最大限制只有12个。

Index Page: 不说了,用来存储索引的。

Data Page: 好了,好了,重点来了,Data Page就是来存储数据的了,我们序化后的Bson文档,先被DataBlock封装一下,先不管它结构,反正是一堆数据,咋存储在DataPage里面的呢?

这就会遇到两情况:

1、如果DataBlock的数据比DataPage可以存储的数据小。

这种情况就很简单了,既然小,那直接写进去就好了。

2、如果DataBlock的数据比DataPage可以存储的数据大。

这种情况就麻烦了,一个Page的大小才4096字节,随便一个大点的超过4k 的Bson文档都塞不进去。有人说那就多用几个DataPage来存储呀,这也是个解决办法,但是不是最优的,LiteDB采用一个或者多个Extend Page来存储剩余的数据,这有什么好处呢?

这种设计理念保证了一个DataPage可以对应多个类的数据,或者一个DataPage只对应一个类的数据,而不会出现一个类的数据对应多个DataPage。

这样我们在遍历DataPage的时候就可以读取到独立的、唯一的DataBlock。如果一个DataBlock在多个DataPage中保存,遍历DataPage的时候,DataBlock就会重复,这样我们需要跟多的字段来存储更多信息,并需要在逻辑上对DataBlock进行去重。

而以现在的模式来看的话,我们只需要遍历一遍Data Page就可以取出所有独立的BsonDocument,简单高效。再者,通过索引来找的话,extend Page里面的数据完全是多余的,不用读取的。

Extend Page:如上所述,用于存储Data Page中还没存储完的DataBlock的数据。所有的Page都是有Pre Next标记的链表,可以从一个Page链接到下一个Page。所以如果一个Extend Page还不足以存储完数据的话,建立更多的Extend Page直到把数据存储完。

Empty Page: 删除掉的Page就标记为空的,可以供下一次数据的存储.

六、一个类的完整存储过程?

好了,最后总结一下,看看我们调用insert(classA)数据库是如何存储的?

1、调用BsonMap把ClassA转化成二进制数据的Bson Document。

2、用DataBlock把BsonDocument进行一次封装。DataBlock定义了Position即在一个Page中的读取地址,以及Extend Page的信息。

3、把DataBlock写入一个Data Page中,多余的数据用Extend Page来存储。

4、把索引字段取出来,封装成Index Node,并写入Index Page。

七、LiteDB的缺点?

从如上的机制我们可以看到LiteDB的4K分页机制还是会造成很多空间的浪费。其次,如果不加索引的话,不仅要全盘扫描,还要要把Bson文档反序化成类,再进行比较,速度还是比较慢的。

吵吵微信朋友圈,请付款实名加入:

吵吵 吵吵

发表评论

电子邮件地址不会被公开。 必填项已用*标注