设计理念上,我们采用数据表的形式对元数据进行了持久化,可通过文件持久化保存元数据,并能根据文件在内存中重建元数据在内存中的表示。我们支持的功能包括
- 元数据的持久化
- 库、表、索引、字段的元数据查询
- 数据索引的创建和删除
- 数据表的创建和删除
- 数据库的创建、加载、切换和删除
我们使用数据表的方式管理元数据。我们将所有的元数据都存在数据表中,所有的元数据表构成了一个系统数据库(System)。具体来说,我们将元数据分为三张表——数据库元数据表、表元数据表、索引源数据表,分别存储数据库、表、索引相关的元数据信息。对于这三张表的元数据信息,因为这三张表的结构的不变性,我们采用类成员变量的形式,将这几张表的字段信息等元数据内嵌在代码中,以此维护元数据数据库。
三张表存储的元数据实际内容和类型如下。
字段名 | 内容说明 | 字段类型 |
---|---|---|
name | 数据库名 | String |
字段名 | 内容说明 | 字段类型 |
---|---|---|
tableName | 表名 | String |
databaseName | 数据库名 | String |
fields | 字段信息 | String |
pkIndex | 主键索引在索引list中的位置 | Int |
字段名 | 内容说明 | 字段类型 |
---|---|---|
tableName | 表名 | String |
databaseName | 数据库名 | String |
columnsInfo | 索引用到的字段信息 | String |
isUnique | 索引是否是unique | Bool |
- fields:是描述数据库表字段信息的字符串(filed)的列表,field存储的信息包括
- 字段名
- 字段类型
- 字段是否可空
- 字段是否为自增字段
- 当前自增起点
- columnsInfo:当前索引所用到的表的字段信息列表,由字段在字段列表中的索引组成
由于元数据以数据表的形式存储,所以对于元数据的直接操作就是对于普通数据表的增删改查操作。
我们支持了各层次元数据的查询接口,包括对于数据库元数据的查询、数据表元数据的查询、和字段元数据的查询,每个种类的元数据的查询都提供了存在性查询(待查询对象是否存在于现有数据库中)和数据获取查询(待查询对象的实际值)。所有的元数据查询结果都包含了该指向该对象本身的指针,例如表元数据查询得到的查询结果中包含了指向该表本身的指针。各类元数据查询详细说明如下:
- 查询描述:输入数据库名,查询数据库是否存在。
- 查询实现:通过在数据库元数据表中查找是否存在包含该数据库名的行数据,判断数据库是否存在
- 查询描述:输入数据库名,获取数据库的实际元数据信息
- 获取的元数据信息包括:
- 数据库名
- 数据库中所有的表的元数据
- 获取的元数据信息包括:
- 查询实现:首先使用存在性查询验证数据库存在,如果不存在,则返回空,否则在表源数据表中找到所有隶属于该数据库的表,获取这些表的元数据和数据库名一同作为结果返回。
- 查询描述:输入数据库名、数据表名,查询该数据库中是否存在该表
- 查询实现:在表元数据表中查询是否存在库名等于输入数据库名、表名等于输入数据表明的项
- 查询描述:输入数据库名、数据表名,获取该数据表的实际元数据信息
- 数据表元数据内容:
- 数据表名
- 所属数据库
- 表的字段元数据(所属数据表,字段名、字段类型、非空、自增)
- 表的行数
- 表的所有索引的元数据
- 数据表元数据内容:
- 查询实现:首先执行存在性查询,验证表存在(不存在则返回空),然后在表元数据表中查询获得除索引信息以外的所有元数据,然后在索引元数据表中查找所有隶属于此表的索引项,获取其元数据后和表的其他元数据一起返回。
- 查询描述:输入数据库名、数据表名、列名,查询指定数据库中的指定表是否存在字段名为输入字段名的字段。
- 查询实现:使用表元数据的内容查询获得表的元数据,在表的元数据中查找是否存在名为输入字段名的列
- 查询描述:输入数据库名、数据表名、列名,获取指定数据库中的指定表指定字段的字段元数据。
- 字段元数据信息内容:
- 所属数据表
- 字段名
- 是否是自增属性,自增起点
- 是否可以是空值
- 字段类型
- 字段元数据信息内容:
- 查询实现:首先执行存在性查询,验证字段存在(不存在返回空),使用表元数据的内容查询获得表的元数据,在表的元数据中查找该列元数据信息返回。
- 查询描述:输入数据库名、数据表名,获取该数据表的所有索引的元数据信息
- 单个数据索引元数据信息包括:
- 所属数据表
- 使用到的列的序列号
- 使用到的列的类型
- 索引是否是unique
- 单个数据索引元数据信息包括:
- 查询实现:首先使用表元数据的存在性查询确认表的存在性,然后在索引元数据表中查询属于该表的索引行,将行数据中的元数据信息返回。
涉及数据库元数据变动的操作包括对于数据库、表、索引的增加和删除操作。这些操作通过在对应的源数据表中插入或删除对应的元数据表项,来完成数据库中对应元数据的更新。
- meta Package:meta包中保存了几种对象的元数据类和用来实施元数据查询的方法类
- DatabaseInfo:保存数据库元数据信息的类,保存的信息见上文元数据查询的内容查询
- TableInfo:保存数据表元数据信息的类,保存的信息见上文元数据查询的内容查询
- ColumnInfo:保存字段元数据信息的类,保存的信息见上文元数据查询的内容查询
- IndexInfo:保存索引元数据信息的类,保存的信息见上文元数据查询的内容查询
- Metadata:提供了库、表、字段、索引等所有元数据查询的接口的方法类。
- disk.System:用于维护系统数据库的加载,和系统数据库在内存中的更新,向外提供的接口包括:
- 系统数据库的加载
- 数据库的新建、删除
- 数据索引的新建、删除
- 当前操作数据库的加载、移除、切换
- 文件系统的整体保存
我们将数据文件分为索引文件和数据文件两类,索引文件用于存储索引数的结构,数据文件用于存储数据库中实际的行数据. 实现的功能包括:
- 多种数据类型的持久化存储,包括int,short,long,double,float,string,bool.
- 记录数据的增删改查
- 行数据的高速缓存
- 索引树的部分更新
- 自定义的二进制文件格式存储数据,数据文件——.data, 索引文件——.ndx
将数据存在多个大小相等并且较小的块(block)中,文件主体由block的列表构成,文件头存储文件中块的大小、块的数量。
- 块个数
- 块大小
- 块列表
- 空位图(用bit表示某个属性是否为null,1为null,该属性位置上存储内容无效)
- 变长字段记录内偏移量、长度
- 定长字段的值
- 变长字段的值
例:
空位图 | 变长字段偏移、长度1 | 变长字段偏移、长度2 | 定长字段1 | 定长字段2 | 变长字段1 | 变长字段2 | ||
---|---|---|---|---|---|---|---|---|
值 | 0100 | (21,2) | (23,4) | 12 | null | “he” | "jack" | |
bytes | 4-4 | 5-8 | 9-12 | 13-16 | 17-20 | 21-22 | 23-26 |
采用变长数据的存储方式——分槽的页结构,结构如下图所示,在块头部存储块的元数据。具体存储结构见下文:
- 块大小
- 已经存储的记录个数
- 空闲位置指针
- 空闲空间大小
- 记录条目数组(块内偏移、大小)
- 记录数组
- 如果不是最后一条条目,则将对应记录的条目置为(-1,-1),否则回收条目的空间,
- 将对应索引后的所有记录依次后移
- 更新记录个数、空闲位置指针、空闲位置大小,因为单个块的大小不大,所以移动的代价相对变小。
- 如果条目列表中有空闲条目,则直接使用空闲条目,否则新建条目
- 将数据写入块中,并更新条目上的偏移和大小
-
如果更新后的数据大小变小,则将其后的记录后移相应位置,并更新对应条目的值;
-
如果更新后的数据大小变大,则对原数据执行删除操作后对新数据执行插入操作;
遍历块的数组,
-
找到第一个可以插入的块(剩余大小足够大),在空闲位置指针处新建一个对应记录,更新空闲位置指针、记录条目数组、记录个数、空闲大小
-
如果没有可以插入的块,就新建一个块,然后插入
- 找到待删除的块
- 执行块内删除
- 检查当块是否可以容纳更新后的记录
- 如果可以,执行块内更新
- 如果不行,先删除旧记录,后执行新的记录的插入
将整棵树以节点为单位,随机存储在文件中。文件内的空闲空间以优先列表的形式串接起来,用以新节点的插入。
- 文件头:
- 索引是否是unique;
- 根节点的偏移
- 空链表的头偏移,大小;
- 索引节点列表
- 节点大小
- 节点类型:m中间节点,l叶子节点
- 左节点偏移
- 右节点偏移
- key个数
- (中间节点)子节点的关键值和子节点偏移量|(叶子节点)关键值和对应叶子的内行数据个数和对应行的快和块内索引
中间节点数据格式:
叶子节点数据格式:
将节点对应空间加入空链表:
- 将节点空间加入到空列表中,保持列表按大小升序排序
- 如果更新后的节点大小不大于原节点的大小
- 覆盖原有数据
- 如果更新后的节点大小大于原有节点的大小
- 删除原有节点
- 插入更新后的节点
- 在空列表中找到最适合插入该节点的空间——空间不小于该节点大小的最小空间,并将节点写入。
针对频繁的文件IO可能导致的访问时间过长,我们实现了数据文件在内存中的缓存. 以块为单位,在内存中建立数据文件的缓存空间,数据文件的访问和改写都会先在缓存中操作,在必要的时候才会进行文件的读写.
针对缓存冲突的问题,使用FIFS策略,给每个缓存中的块添加时间戳,当出现缓存区溢出时,将时间戳最小的块移除缓存区
- 如果缓存未满,直接从文件中加载对应块
- 如果缓存已满,根据FIFS策略,将符合条件的块移除缓存
- 如果当前块未被更改,则直接丢弃
- 如果当前块已经被更改,则将文件中对应的存储区域用现在的内容覆盖。