文章目录
ConsumeQueue文件讲解
概述
RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的。多个Topic文件是共用一个CommitLog文件的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。ConsumeQueue文件的引入的目的主要是提高消息消费的性能。
文件结构
消息消费者Consumer可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog(物理消费队列)中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。
ConsumeQueue文件可以看成是基于topic的CommitLog索引文件,故ConsumeQueue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。
同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M。单条记录结构如下:
消息的起始物理偏移量physical offset(long 8字节)+消息大小size(int 4字节)+tagsCode(long 8字节),每条数据的大小为20个字节(这个很重要,源码中有用到这个固定值),从而每个文件的默认大小为600万个字节。
ConsumeQueue类讲解
字段属性
private final DefaultMessageStore defaultMessageStore;
//映射文件队列
private final MappedFileQueue mappedFileQueue;
//消息的Topic
private final String topic;
//消息的queueId
private final int queueId;
//指定大小的缓冲,因为一个记录的大小是20byte的固定大小
private final ByteBuffer byteBufferIndex;
//保存的路径
private final String storePath;
//映射文件的大小
private final int mappedFileSize;
//最后一个消息对应的物理偏移量 也就是在CommitLog中的偏移量
private long maxPhysicOffset = -1;
//最小的逻辑偏移量 在ConsumeQueue中的最小偏移量
private volatile long minLogicOffset = 0;
//ConsumeQueue的扩展文件,保存一些不重要的信息,比如消息存储时间等
private ConsumeQueueExt consumeQueueExt = null;
这里比较重要的属性,topic
,queueId
,maxPhysicOffset
,minLogicOffset
。这里对这几个属性进行说明一下
属性 | 说明 |
---|---|
topic | 文件所属的topic |
queueId | 文件所属的topic下的队列id |
maxPhysicOffset | 最大的物理偏移量,这里指的是CommitLog中的偏移量 |
minLogicOffset | 最小的逻辑偏移量,这里指的是ConsumeQueue中的最小偏移量 |
需要分清楚的是ConsumeQueue是消息的逻辑地址文件,CommitLog是消息的物理地址文件。
内部方法解析
构造方法
ConsumeQueue
只有一个构造方法。
public ConsumeQueue(
final String topic,
final int queueId,
final String storePath,
final int mappedFileSize,
final DefaultMessageStore defaultMessageStore) {
//指定文件的存储位置
this.storePath = storePath;
//指定文件大小
this.mappedFileSize = mappedFileSize;
//指定DefaultMessageStore对象
this.defaultMessageStore = defaultMessageStore;
//存储指定topic消息
this.topic = topic;
//指定指定queueId消息
this.queueId = queueId;
//设置对应的文件路径,$HOME/store/consumequeue/{topic}/{queueId}/{fileName}
String queueDir = this.storePath
+ File.separator + topic
+ File.separator + queueId;
//创建文件映射队列
this.mappedFileQueue = new MappedFileQueue(queueDir, mappedFileSize, null);
//创建20个字节大小的缓冲
this.byteBufferIndex = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE);
//是否启用消息队列的扩展存储
if (defaultMessageStore.getMessageStoreConfig().isEnableConsumeQueueExt()) {
//创建一个扩展存储对象
this.consumeQueueExt = new ConsumeQueueExt(
topic,
queueId,
//consumeQueueExt的存储地址
StorePathConfigHelper.getStorePathConsumeQueueExt(defaultMessageStore.getMessageStoreConfig().getStorePathRootDir()),
//todo 设置消费队列文件扩展大小 默认48M
defaultMessageStore.getMessageStoreConfig().getMappedFileSizeConsumeQueueExt(),
//todo 位图过滤的位图长度
defaultMessageStore.getMessageStoreConfig().getBitMapLengthConsumeQueueExt()
);
}
}
构造方法中没有除了设置字段值之外的额外的逻辑。都是比较简单的逻辑,不多进行分析。
文件加载load
load
方法调用也是在RocketMQ的Broker启动的时候,会调用到,用来加载机器内存中的ConsumeQueue文件
public boolean load() {
//从映射文件队列加载
boolean result = this.mappedFileQueue.load();
log.info("load consume queue " + this.topic + "-" + this.queueId + " " + (result ? "OK" : "Failed"));
//存在扩展存储则加载
if (isExtReadEnable()) {
//消息队列扩展加载=》
result &= this.consumeQueueExt.load();
}
return result;
}
服务重启时修复文件的recover
RocketMQ在启动时候,会去尝试恢复服务器中的ConsumeQueue文件。文件恢复的逻辑就是通过检查每个消息记录单元中记录信息来判断这个记录是否完整,进而分析整个文件是不是完整,最后对文件中损坏的记录进行截断。整体的恢复逻辑有点长。这里对每个消息单元的分析是基于单个消息单元的长度是20个字节长度的原理来进行分析。
public void recover() {
final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
if (!mappedFiles.isEmpty()) {
//如果文件列表大于3就从倒数第3个开始,否则从第一个开始
int index = mappedFiles.size() - 3;
if (index < 0)
index = 0;
//获取consumeQueue单个文件的大小
int mappedFileSizeLogics = this.mappedFileSize;
//获取最后一个映射文件
MappedFile mappedFile = mappedFiles.get(index);
ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
//映射文件处理的起始偏移量
long processOffset = mappedFile.getFileFromOffset();
long mappedFileOffset = 0;
long maxExtAddr = 1;
while (true