Kahadb设计思想
简介
- hakadb是activemq的持久化数据库,作为消息队列的存储,每个消息有一个消息ID,提供了对消息的快速的查找,更新,以及消息的事物支持,以及意外磬机之后的恢复。丰富的功能决定了他在存储结构上与redis中简单的双端队列不同。
kahadb三大部分
- 主要由索引(B-Tree Index),数据(PageFile),日志(Journal)组成。为了实现消息的快速定位更新,通过b-tree来索引数据,主要对应目录中的;为了数据的写入效率,数据都是顺序存储在文件中,这部分工作被抽象到了PageFile类;Journal负责了操作日志,用于写入和恢复最终都是决定与Journal
kahadb之Journal
核心类
Journal 读写日志的facade
- write方法,向日志系统写入日志。返回Location。
- read方法,根据Location从之日系统中读取日志
Location 日志系统中定位日志Record
- 由日志的DataFileId,offset,size,type组成,用来定位日志
DataFile 日志文件的存储文件
日志都存储在db-{1,2,3,...n}.log文件中,每个这个文件都由DataFile类来封装,其中1,2,3..就是DataFileId,还有一些长度和文件指针的信息
DataFileAppender 多线程下优化了批量写入
- 两个线程,一个队列,把并发写变成批量单线程写入,具体实现略
DataFileAccessorPool和DataFileAccessor
- 封装了对日志的读操作,暂时略,对理解整体结构没有大的影响。
读写实例
File dir = new File("target/tests/Test");
dir.mkdirs();
Journal dataManager = new Journal();
dataManager.setDirectory(dir);
dataManager.start();
//写入日志
ByteSequence write = new ByteSequence("Hello world".getBytes());
Location location = dataManager.write(write, true);
//读取日志
ByteSequence read = dataManager.read(location);
System.out.println(new String(read.data));
存储结构
- 由头部28个字节,和数据组成
头部
- 四个字节Journal.BATCH_CONTROL_RECORD_SIZE, 固定的28
- 一个字节Journal.BATCH_CONTROL_RECORD_TYPE, 2
- 十一个字节,字符串 Journal.BATCH_CONTROL_RECORD_MAGIC, WRITE BATCH 这几个字节
- 四个字节记录批量写入了多少批的BATCH_SIZE ,不固定的int
- 八个字节校验和,后半部分写入日志的校验和
数据
- 四个字节的size,这个size是数据头部的长度+数据的长度
- 一个字节数据类型
- 若干字节的数据
日志文件的RecoveryCheck
- 在第一次加载日志文件的时候会做一次Recovery,主要是恢复一些上次写入的Offset,知道了存储结构以后其实按照这个结构读写就可以找到最后一次写的Offset,但是假如日志写入一半失败到了,导致部分存储结构不整齐了,但后续是好的,理想情况应该跳过这些坏的部分,让后面的可用。Recovery的时候对这种情况会重新在文件里搜索日志的头部的前16字节,不知为何是要搜这头部,搜到以后就把之前搜索过的区域标记为corruptedBlocks
kahadb之PageFile
核心类
PageFile
PageFile负责了消息的持久化存储。PageFile对外的主要接口有删除数据库,载入数据库,生成一个用于读写的Transaction,数据被存储在了
Page
PageFile如果是用于存储数据的文件,Page就是文件中的每一小段,为了减少IO,每个文件被分成4K大小的段,是一个Page。至于为什么4K大小可以减少磁盘IO的原因是,当用户程序请求要读1K的数据时候,操作系统会预读更多的数据,放到内核缓冲中,因为内核中的缓冲一页的大小是4K,所以应用程序一般都会和操作系统的PageCache对齐,这样充分利用操作系统的PageCache。Page最重要的一个属性是PageId,这个Id是在一个PageFile中顺序排列的,所以根据这个PageId就可以定位到PageFile文件中的Offset。
Transaction
Transaction负责了创建Page,在Page上读写,提交以及回滚操作
读写实例
File dir = new File("target/tests/Test");
dir.mkdirs();
PageFile pf = new PageFile(dir, "myfile");
pf.load();
//写入
Transaction tx = pf.tx();
Page<String> page = tx.allocate();
page.set("Hello world");
tx.store(page, StringMarshaller.INSTANCE, true);
tx.commit();
pf.unload();
pf.load();
//读取
tx = pf.tx();
tx.load(page.getPageId(), StringMarshaller.INSTANCE);
String hello = page.get();
存储结构
- 存储的时候包括{dbname}.data和{dbname}.redo {dbname}.free文件。data文件是保存真正消息的文件。当data文件META信息中的cleanShutdown为false的时候会通过redo文件来做recovery。
.data文件
头部
- 4K的第一页存放META信息
- META初始信息如下
fileType=org.apache.activemq.store.kahadb.disk.page.PageFile
pageSize=4096
freePages=-1
cleanShutdown=true
metaDataTxId=0
fileTypeVersion=1
lastTxId=0
- 需要说明一点的是这个信息在第一页中存储了两遍
BODY部分
- 一个字节的type 固定的是2
- 八个字节的transactionId
- 八个字节的下一个记录的位置
- 四个字节的校验和,不过是个保留字节,没什么用
- N个字节真正的数据
.redo文件
头部
- 八个字节的下一个TX的id 存储在第一页
- 八个字节的校验和 存储在第一页
- 四个字节的当前这个写入的记录数量 存储在第一页
BODY部分
- 8个字节PageId
- page数据与data文件中的body部分相同
- pageId和page数据是一页的大小4K
PageFile的多级缓存
- 可以看到当写入数据的时候分配新的Page,Page写入磁盘是比较消耗IO的,所以在一个Transaction上有一个freeList来保存空的Page,在PageFile也有freeList来加速新分配Page。同时为了加快读写Page的速度,PageFile中有一个PageCache
事物的回滚
- 事物只有在commit的时候才会调用PageFile的write方法,Transaction中的store只是把他逻辑保存在Transaction中,但是回滚的时候要把PageFile中申请的Page归还到freeList