RocketMQ 之 ConsumeQueue

2,486 阅读3分钟

前沿概述

在之前 CommitLog 的文章中,我们分析了 CommitLog 是如何被加载以及存储的。 CommitLog 里的消息是不分主题的,消费者根据当前 CommitLog 偏移,查找该主题下一个消费偏移,成本会非常高。 ConsumeQueue 因此而被设计出来,用作 CommitLog 的索引文件。 并且,只有消息被写入 ConsumeQueue 的时候,消费者才能消费到该消息。因为消息者面对的是 ConsumeQueue

ConsumeQueue 介绍

ConsumeQueue 文件结构

  1. 文件组织方式 consumeQueue 文件组织方式为 topic/queueId/file 三层 image.png 其中 test%TopicTest 为 topic, 2 为 queueId, 00000000000000000000 为 file
  2. 子条目结构 image.png consumeQueue 每个子条目为固定的 20 个字节,这是为了方便计算消费偏移,以及索引而设计的。
  3. file 特点 每个 file 都有 30w 个子条目,每个条目20 个字节。则一个 file 大约为 5.7mb

ConsumeQueue 类抽象

ConsumeQueue 类可以理解为就是对存储结构 topic/queueId/file 的抽象。 然后 ConsumeQueue 有多个,被定义在 DefaultMessageStore 中。

public class ConsumeQueue {
    // ...
    // 每个 队列下,会有很多的 File, 所以这边是个 队列
    private final MappedFileQueue mappedFileQueue;
    private final String topic;
    private final int queueId;
    // ...
}

消息转存线程 - ReputMessageService

在概述中有提到,只有当 CommitLog 的消息被转存到 ConsumeQueue 之后,消费端才能够消费。而转存的工作,RocketMQ 交由了 ReputMessageService 来处理。 这边阐述下 ReputMessageService 是如何工作的。

该线程,每隔 1ms 执行一次以下逻辑。具体见 ReputMessageService#run

  1. ReputMessageService 线程记录着当前转发偏移 reputFromOffset,当 reputFromOffset 偏移量小于 CommitLog 内存的写入偏移量(commitlog 此时未落盘),就会一直转发.
  2. 根据 reputFromOffsetCommitLog 中找到对应的消息, 并 依次 转发给 ConsumeQueue, IndexFile
  3. ConsumeQueue 调用 write() 方法写入消息(未刷盘),供消费者拉取。

ReputMessageServicereputFromOffset 值,会在 ReputMessageService 被初始化的时候,从多个 consumequeue 中去 commitlog 的最大偏移量。还记得 consumequeue 子条目的结构吧, commitlog offset, size, tag hashcodereputFromOffset 的值就是 commitlog offset + size

具体见 DefaultMessage#start()

刷盘线程 - FlushConsumeQueueService

在讲解 ReputMessageService 时,有提到 consumequeue 只会调用 write() 方法写入消息,但是 write() 并不会将消息刷入磁盘。因此 RocketMQ 提供 FlushConsumeQueueService 线程处理 consumequeue 刷盘

FlushConsumeQueueService 每隔 1 秒 执行1次刷盘动作。

FlushConsumeQueueService 刷盘逻辑如下

  1. 新写入内容不足 2页 时,不刷盘
  2. JVM 要退出时会被强制刷盘
  3. 60s 内,为执行任何刷盘,会被强制刷盘
  4. 遍历所有队列,依次刷盘

从刷盘流程来看,整体和 CommitLog 刷盘线程非常像。区别在于参数配置的不同

代码位置:FlushConsumeQueueService#run()

可配置刷盘参数

参数默认值作用
flushIntervalConsumeQueue1000 毫秒间隔多次时间执行强制刷盘动作
flushConsumeQueueLeastPages2至少凑够 2 页,才会执行刷盘
flushConsumeQueueThoroughInterval60000 毫秒间隔多久未刷盘,会被强制刷盘

ConsumeQueue 刷盘设计思考

前面,我们提到,只有消息被写入 ConsumeQueue,消费端才能拉取到消息。 RocketMQ 为了让消费端尽快消费到写入的消息,在写入时,仅写入内存,并未持久化到磁盘上。 实际上,这样的设计,在需要与磁盘打交道的中间件中,都可以看到。