1.RocketMQ实时更新消息消费队列 索引文件

当消息生产者提交的消息存储在CommitLog文件中,ConsumeQueue,IndexFile需要及时更新,RocketMQ通过开启一个线程ReputMessageServcie来准实时转发CommitLog文件更新事件,相应的任务处理器根据转发的消息及时更新ConsumeQueue,IndexFile文件

if (this.getMessageStoreConfig().isDuplicationEnable()) { 
	this.reputMessageService.setReputFromOffset(this.commitLog.getConfirmOffset()); 
} else { 
	this.reputMessageService.setReputFromOffset(this.commitLog.getMaxOffset()); 
  this.reputMessageService.start(); 
}

Broker 服务器在启动时启动 ReputMessageService 线程,并初始化一个非常关键的ReputFromOffset参数,该参数含义是ReputMessageService 从哪个物理偏移 开始转发消息ConsumeQueu和 IndexFile, 如果允许重复转发, reputFromoffset设置CommitLog提交指针;如果不允许重复转发, reputFromOffset设置为Commitlog 的内存中最大偏移量

@Override
public void run() {
    DefaultMessageStore.log.info(this.getServiceName() + " service started");
    while (!this.isStopped()) {
        try {
            Thread.sleep(1);
            this.doReput();
        } catch (Exception e) {
            DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }
    DefaultMessageStore.log.info(this.getServiceName() + " service end");
}

ReputMessageService 线程每执行一次任务推送休息1毫秒就继续尝试推送消息到消息消费队列和索引队列

private void doReput() {
   
      //返回 reputFromOffset偏移量开始的全部有效数据(commitlog 文件.然后循环读取每一条消息
        SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
        if (result != null) {
                for (int readSize = 0; readSize < result.getSize() && doNext; ) {
   //从result返回的ByteBuffer中循环读取消息,一次读取一条,创建DispatchRequest对象
         DispatchRequest dispatchRequest =
                        DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
   //最终将分别调用 CommitLogDispatcherBuildConsumeQueue (构建消息消费队 )、CommitLogDispatcherBuildlndex (构建索引文件)
                  if (dispatchRequest.isSuccess()) {
                    if (size > 0) {
                      DefaultMessageStore.this.doDispatch(dispatchRequest);               
        } else {
            doNext = false;
        }
}
  1. 返回 reputFromOffset偏移量开始的全部有效数据(commitlog 文件.然后循环读取每一条消息
  2. 从result返回的ByteBuffer中循环读取消息,一次读取一条,创建DispatchRequest对象
  3. 最终将分别调用 CommitLogDispatcherBuildConsumeQueue (构建消息消费队 )、CommitLogDispatcherBuildlndex (构建索引文件)

2.根据消息更新 ConumeQueue

消息消费队列转发任务实现类为 CommitLogDispatcherBuildConsumeQueue ,内部终将调用 putMessagePositioninfo 方法

public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
    ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
    cq.putMessagePositionInfoWrapper(dispatchRequest);
}

根据消息主题与队列 ID ,先获取对应的 ConumeQueue ,其逻辑 较简单,因为每一个消息主题对应一个消息消费队列目录 ,然后主题下每一个消息队列对应 多个文夹,然后取该文件夹最后 ConsumeQueue 件即可

3.根据消息更新 Index 索引文件

Hash 索引文件转发任务实现类 CommitLogDispatcherBuildlndex

if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
    DefaultMessageStore.this.indexService.buildIndex(request);
}

如果 messsagelndexEnable设置为true ,则调用 IndexService#buildlndex 构建 Hash引,否则忽略本次转发任务

public void buildIndex(DispatchRequest req) {
    IndexFile indexFile = retryGetAndCreateIndexFile();
    if (indexFile != null) {
        long endPhyOffset = indexFile.getEndPhyOffset();
        DispatchRequest msg = req;
        String topic = msg.getTopic();
        String keys = msg.getKeys();
        if (msg.getCommitLogOffset() < endPhyOffset) {
            return;
        }
   	}

获取或创建 IndexFile 文件并获取所有文件最大的物理偏移量 如果该消息的物理偏移 小于索引文件中的物理偏移,则说明是重复数据,忽略本次索引构建

if (req.getUniqKey() != null) {
    indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
    if (indexFile == null) {
        log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
        return;
    }
}

如果消息的唯一键不为空 ,则添加到 Hash 索引中,以便加速根据唯一键检索消息

if (keys != null && keys.length() > 0) {
    String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
    for (int i = 0; i < keyset.length; i++) {
        String key = keyset[i];
        if (key.length() > 0) {
            indexFile = putKey(indexFile, msg, buildKey(topic, key));
            if (indexFile == null) {
                log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
                return;
            }
        }
    }
}

构建索引键, RocketMQ 支持为同一个消息建立多个索引,多个索引键空格分开

4当Broker上的数据存储超过一定时间之后,磁盘数据是如何清理的?

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        DefaultMessageStore.this.cleanFilesPeriodically();
    }
}, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);

这个调度任务⾥就会执⾏DefaultMessageStore.this.cleanFilesPeriodically()⽅法,其实就是会去周期性的清理掉磁盘 上的数据⽂件,也就是超过72⼩时的CommitLog、ConsumeQueue⽂件

private void cleanFilesPeriodically() {
    this.cleanCommitLogService.run();
    this.cleanConsumeQueueService.run();
}

⾥⾯包含了清理CommitLog和ConsumeQueue的清理逻辑,

在清理⽂件的时候,他会具体判断⼀下,如果当前时间是预先设置的凌晨4点,就会触发删除⽂件的逻辑,这个时间是 默认的;或者是如果磁盘空间不⾜了,就是超过了85%的使⽤率了,⽴马会触发删除⽂件逻辑。 上⾯两个条件,第⼀个是说如果磁盘没有满 ,那么每天就默认⼀次会删除磁盘⽂件,默认就是凌晨4点执⾏,那个时候 必然是业务低峰期,因为凌晨4点⼤部分⼈都睡觉了,⽆论什么业务都不会有太⾼业务量的

第⼆个是说,如果磁盘使⽤率超过85%了,那么此时可以允许继续写⼊数据,但是此时会⽴马触发删除⽂件的逻辑; 如果磁盘使⽤率超过90%了,那么此时不允许在磁盘⾥写⼊新数据,⽴马删除⽂件。这是因为,⼀旦磁盘满了,那你写⼊磁盘会失败,此时你MQ就彻底故障了。

所以⼀旦磁盘满了,也会⽴马删除⽂件的。 在删除⽂件的时候,⽆⾮就是对⽂件进⾏遍历,如果⼀个⽂件超过72⼩时都没修改过了,此时就可以删除了,哪怕有 的消息你可能还没消费过,但是此时也不会再让你消费了,就直接删除掉。

5.消息队列与索引文件恢复

RocketMQ 存储首先将消息全量存储在 Commitlog 文件中,然后异步生成转发任务更新 ConsumeQueue, Index 文件 .如果消息成功存储到 Commitlog 文件中,转发任务未成功执行,此时消息服务器 Broker 于某个原因宕机,导致 CommitlogconsumeQueue,IndexFile 文件数据不一致,如果不加以人工修复的话,会有一部分消息即便在 Commitlog文件中存在,但由于并没有转发到 Consumqueue,这部分消息将永远不会被消费者消费

怎么处理?

异常文件恢复的步骤与正常停止文件恢复的流程基本相同,其主要差别有两个 首先,正常停止默认从倒数第三 件开始进行恢复, 而异常停止需要从最后一个文件 往前走 ,找到第 一个消息存储正常的文件 其次,如果 commitlog目录没有消息文件,如果在消息消费 队列目 录下存在文件,则需要销毁