ActiveMQ源码分析之KahaDB消息存储

KahaDB是ActiveMQ从版本5.4之后的默认消息存储引擎,消息存储机制是消息中间件最重要的核心部件和性能提升点。KahaDB的存储主要分为索引存储和消息存储部分。

KahaDB的索引是B+树,而其最基础的存储机制则是基于“随机存取文件”设计的。相对于连续存储的文件来说这种方式比较耗时,但是设计思路比较简单。

相对的,Kahadb的消息即数据存储机制则是基于“连续存储”文件设计的,这种存储方式的效率高,但是空间的重复利用率低。

Kahadb中磁盘文件介绍:

           *.data文件,保存的是索引内容

           *.redo文件,保存的是针对索引文件的恢复内容

           *.free文件,保存的是索引文件中空闲页的页码

           db-*.log文件,保存的是索引所指向的消息的信息内容

本篇主要分析KahaDB消息存储实现机制。Producer发送给Broker的消息会被封装成KahaAddMessageCommand对象,包括destination,messageId,transactionInfo,message等属性,最终KahaAddMessageCommand的数据结构如下:

transaction_info {
  local_transaction_id {
    connection_id: ID:jiangzhiqiangdeMacBook-Pro.local-49633-1556954797292-1:1
    transaction_id: 1
  }
}
destination {
  type: QUEUE
  name: FirstQueue
}
messageId: ID:jiangzhiqiangdeMacBook-Pro.local-49633-1556954797292-1:1:1:1:1
message: org.apache.activemq.protobuf.Buffer@ffffffda
priority: 4
prioritySupported: false

接着调用MessageDatabase的store方法,将这部分需要保存的数据转换成ByteSequence对象,保存在byte数组中。后面对数据的操作都转换成了对byte数组的操作。

数据准备好之后,就准备开始往磁盘中写数据了。AMQ中负责写数据的是DataFileAppender类。

storeItem()方法中创建了Location对象,该对象主要记录数据在磁盘上的存储位置。Location类有三个非常重要的属性:1、dataFileId表示消息所在的log文件,例如消息保存在db-1.log中,则dataFileId的值为db-1.log。2、offset表示在log文件中的偏移量。3、size表示数据大小。方法中首先根据byte数组计算出本次需要写入数据的大小,并将计算的size赋值给Location对象。接着将数据构造成WriteCommand对象,该对象是写入块中的最小单元,用户的每个写入动作都会生成一个writecommand对象。接着生成WriteBatch对象,将writecommand对象保存在链表writes中,用于批量写入磁盘,提升写入效率,同时设置Location的offset和dataFileId。

public void append(Journal.WriteCommand write) throws IOException {
          this.writes.addLast(write);
          write.location.setDataFileId(dataFile.getDataFileId());
          write.location.setOffset(offset+size);
          int s = write.location.getSize();
          size += s;
          dataFile.incrementLength(s);
          journal.addToTotalLength(s);
}

接着将即将要写入磁盘的数据的索引缓存在inflightWrites这个Map中,以提高访问时的效率。

inflightWrites.put(new Journal.WriteKey(write.location), write);

inflightWrites中的索引此时为1:128927,表示dataFileId:offset。数据写入磁盘前的准备工作已基本完成,因为默认写入是异步的,因此数据不会立即写入。

真正负责异步写磁盘的是DataFileAppender类的processQueue方法。

protected void processQueue() {
        DataFile dataFile = null;
        RecoverableRandomAccessFile file = null;
        WriteBatch wb = null;
        try (DataByteArrayOutputStream buff = new DataByteArrayOutputStream(maxWriteBatchSize);) {

            while (true) {
                省略。。。。。。。
                Journal.WriteCommand write = wb.writes.getHead();

                // Write an empty batch control record.
                buff.reset();
                buff.write(EMPTY_BATCH_CONTROL_RECORD);

                boolean forceToDisk = false;
                while (write != null) {
                    forceToDisk |= write.sync | (syncOnComplete && write.onComplete != null);
                    buff.writeInt(write.location.getSize());
                    buff.writeByte(write.location.getType());
                    buff.write(write.data.getData(), write.data.getOffset(), write.data.getLength());
                    write = write.getNext();
                }

                // append 'unset', zero length next batch so read can always find eof
                buff.write(Journal.EOF_RECORD);

                ByteSequence sequence = buff.toByteSequence();

                // Now we can fill in the batch control record properly.
                buff.reset();
                buff.skip(RECORD_HEAD_SPACE + Journal.BATCH_CONTROL_RECORD_MAGIC.length);
                buff.writeInt(sequence.getLength() - Journal.BATCH_CONTROL_RECORD_SIZE - Journal.EOF_RECORD.length);

                // Now do the 1 big write.
                file.seek(wb.offset);
                file.write(sequence.getData(), sequence.getOffset(), sequence.getLength());

                ReplicationTarget replicationTarget = journal.getReplicationTarget();
                if( replicationTarget!=null ) {
                    replicationTarget.replicate(wb.writes.getHead().location, sequence, forceToDisk);
                }

                if (forceToDisk) {
                    file.sync();
                }

                Journal.WriteCommand lastWrite = wb.writes.getTail();
                journal.setLastAppendLocation(lastWrite.location);

                signalDone(wb);
            }
        } catch (Throwable error) {
            省略。。。。。。
        }

方法中首先获取之前构造的WriteBatch对象,该对象中封装了一个WriteCommand对象的链表writes。获取writes的头结点wb.writes.getHead(),然后从pos为0处开始写入批写入标志数据EMPTY_BATCH_CONTROL_RECORD,接着写入数据的大小size,数据的类型type,最后写入数据的内容data。紧接着获取头结点的下一个结点write = write.getNext(),直到处理完writes的尾结点。最后写入结尾标志buff.write(Journal.EOF_RECORD); 注意,此处的写入是指将WriteBatch中封装的数据写入buff中,此时数据还在内存中,还没有写入磁盘。

从file.seek(wb.offset);这一句开始真正开始写磁盘,表示本次新增的数据从offset表示的位置开始写入,到file.sync()强制将数据写入文件,整个写盘流程结束。从这里我们可以看出KahaDB数据写入的方式为顺序写入,后面每次新增的数据都是在之前的数据后面新增,提高了写数据的效率,而且将writes中的数据一次性写入磁盘,实现了批量写入的功能,也提升了写磁盘的效率。

最终signalDone方法将inflightWrites中缓存的索引及数据清除,结束整个写入操作。

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值