.dat文件写入byte类型数组_保存B-Tree到文件中 - BTree序列化

上篇文章中,我们一起实现了一个内存版支持多线程BTree,它基本上是一个功能较为完整的BTree,支持增删查数据,但是它并不能保存数据到文件,或者说,一旦程序关闭,BTree中的数据将全部丢失,作为一个数据库存储引擎,这个不能接受,故本篇文章中,我们将讨论如何实现保存BTree到文件中。

LSN

把内存中的BTree写入文件,就是要把BTree下面的Node(ValueNode,LeafNode和InterNode)都写入文件中,但是写入文件并不能直接把内存数据直接写入文件,比如指针或者引用。LeafNode和InterNode中有一个字段或者成员变量:

//指向子节点的数组

这个字段是指向节点的子节点引用,这个值是一个内存地址,直接写入文件以后,下次再读出来以后,可能子节点已经不在这个内存值指向的内存位置了。所以,需要增加一个新的字段:

//指向文件中子节点的数组

这个字段用于存储每个子节点的LSN,LSN是文件中的偏移地址(offset),当子节点写入文件以后,一定会有一个LSN,用于表示唯一标识该节点。这样,当查询子节点时,可以通过LSN来查询子节点,不一定要用子节点引用。

Node序列化

节点写入文件前,需要先序列化为可以写入文件的类型,一般为byte数组类型,Java语言不能直接把内存对象转换为byte数组,需要专门处理(即使可以直接转换成byte数组,也不能直接转换,因为有些字段不能直接转,比如子节点的指针)。

为了让所有Node都能实现序列化,新增加一个接口,并且有一个需要实现的函数writeIntoBuffer:

public 

所有的节点都是实现这个接口:

//InterNode和TreeNode
//ValueNode

参数buffer存储Node序列化后的结果,这些结果通一个新类LogManager写入到文件中。

LogManager类

LogManager是一个单例,这样可以在任意需要的地方调用,LogManager中实现一个write方法或者函数,write可以将实现Loggable的对象写入到文件中。

文件格式

一个Loggable对象写入文件中需要一定的格式,不能随意写入,这样才能保证能够在需要这些对象时,成功从文件中读取出来。一般Loggable对象的文件格式包括两个部分:Header和Content,Header部分大小固定,Content部分每个不懂类型的Loggable都不相同(与TCP报文协议有点像)。Header包括两个部分:Loggable在文件中的大小以及类型;Content则是序列化后的Loggable对象。如下图示:

61197611802f8ca9d266ab9869fc36c3.png

LogHeader

Header部分需要专门的类来处理:LogHeader。LogHeader主要有两个字段:LogType和LogSize,同时LogHeader也需要支持序列化。LogHeader还定义了各种不同类型的LogType,现在主要的LogType有ValueNode,LeafNode以及InterNode,所以LogHeader如下:

public 

LogSize和LogType

LogSize是节点写入文件以后的大小,这个大小和内存大小不一样, 需要专门计算,TreeNode和ValueNode的计算方法如下:

//TreeNode计算LogSize
//计算ValueNode的LogSize

LogHeader存储的LogType需要每个Loggable提供,因此Loggable需要一个新的方法或者函数:getLogType。InterNode,LeafNode和ValueNode需要是这个函数,返回其类型。

Log Structure Write

讨论了Node写入文件的格式以后,就需要考虑Node应该在写入文件哪个位置。写入文件的位置一般有两种:

1. 每个Node都一个固定的位置,这要求Node的LogSize必须固定,也就要求Node的内存大小也要固定,所以需要把内存划分成一个个固定的Page或者Block,这样可以Node可以写入固定的位置,这个方法在很多传统数据库中会使用;

2. 还有一个方法就是每次写入的Node都写入文件的尾部,这样就不要求Node大小固定,同时这样写入的性能会更加好一些,这种方式成为Append-Only或者Log Structure Write,很多新兴的存储引擎使用这个方法,LSM也是使用这种方式写入文件。

07c44aee2e468a11044d02dfc234ffb0.png

本文中,我们选择第二种方法,LogManager的write函数实现这个方法。

public 

Node的写入

当程序关闭时,需要将所有的节点写入文件中,以便下次使用,Node的写入就在BTree的close中实现。

写入方法

Node写入之前需要写入它的子节点,这是因为Node需要写入子节点的LSN,而子节点只有写入才有LSN,所以需要先写入子节点。这个写入方法就是后根写入或者后序写入,其过程如下:

80f0401961d4005339737a898cfd6403.png

为了实现这个方法,节点TreeNode需要增加一个新的方法writeChildren:

@Override
    

ValueNode也需要实现一个空的writeChildren,因为ValueNode没有子节点。

BTree的close方法则是先写入根节点的子节点,然后写入根节点:

public 

BTree读取

实现从文件中读取BTree之前,需要先实现将Node从字节数组中反序列化,也就是实现Loggable中的readFromBuffer方法:

//TreeNode的readFromBuffer

LogHeader也需要实现一个readFromBuffer的方法:

public 

LogManager也需要一个支持通过LSN从文件中读取出Node的方法,其做法为根据LSN,跳转到文件对应的offset位置,然后先读取固定大小的LogHeader,然后根据LogHeader中的LogType,创建一个对应的空Node,接着根据LogHeader中的LogSize读取相同大小的字节数据,然后使用readFromBuffer反序列化空的Node成为一个真正的Node:

public 

寻找根节点

BTree的访问都是从根节点开始的,所以读取BTree需要先读取根节点,才能进一步读取其他节点。BTree所有节点都写入文件后,会有一个根节点的LSN,所以可以根据这个LSN来读取根节点,但是如何保存根节点LSN?有两种方法:

  1. 人为的记录LSN,这个方法不可取,因为程序是自动的,不能靠人工介入(当然今天很多看起来很自动的应用或者程序,后面都可能是人工介入的),另外人工记忆也不一定可靠
  2. 将根节点LSN也写入文件,在需要的时候,扫描整个文件取得LSN。本文选择该方法。

所以我们需要修改LogManager实现一个rootLSNWrite的方法以及BTree的close方法写入rootLSN

public 

根节点LSN读取

读取BTree之前需要读取根节点LSN,而此时没有任何的LSN信息,所以需要从头到尾扫描整个文件,找到最后一个LOG类型为ROOT_LSN_LOG的记录,然后从其中读取根节点LSN,这个操作需要BTree的构造方法中执行:

public 

子节点读取

以前子节点可以直接通过childList[i]获取,现在不能使用这种方式,因为childList[i]可能是空的,需要其他方法实现,实现的方式是先判断childList[i]是否为空,如果不为空返回该值,如果为空,根据lsnList[i]从文件中读取该节点:

protected 

其他修改

BTree还需要一些修改,在put方法中,发现rootNode为空,而且rootLSN不为-1是,需要从文件中读取rootNode,而不是新建一个:

if 

在delete以及get方法中,如果rootNode为空,且rootLSN不为空,则需要从文件中读取该节点:

if 

优化

根据上面的讨论,我们可以实现一个支持读写文件的BTree,但是这个实现有一个问题,就是每次关闭BTree时,所有的节点都会重新写入文件,即使没有做任何修改。这样是比较浪费硬盘空间,需要优化一下。首先我们做一个定义,所有不是从文件中读取的Node都为dirty的,包括新建立的Node, 任何读写操作,修改count,keyList以及lsnList都需要设置Node为Dirty;Dirty的Node才需要写入文件,非dirty的node不用写入文件。这样writeChildren方法需要做一些修改:

@Override
    

代码

本文中所有实现的代码,都将在下一篇文章中给出

结论

本文讨论了如何把BTree写入文件中,实现了内存数据保留的方法,同时也介绍了如何读取这些数据,特别是RootLSN,这样就实现了一个基于文件的BTree,但是内存是有限的,不可能在所有情况一下,内存都足以支持BTree从容调用close方法,把所有的dirty数据都写入文件中,下篇文章,我们将讨论如何处理这种内存不够的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值