概要设计
RocketMQ 的存储文件,放在 ${ROCKET_HOME}/store 目录下
- Commitlog:消息存储文件,所有消息主题的消息都存储在 Commitlog 文件中
- ConsumeQueue:消息消费队列,消息到达 Commitlog 文件后,将异步转发到消息队列,供消息消费者消费
- IndexFile:消息索引文件,主要存储消息 key 与 offset 对应关系
核心消息存储类:DefaultMessageStore
// 消息存储配置属性
private final MessageStoreConfig messageStoreConfig;
// CommitLog 文件的存储实现类
private final CommitLog commitLog;
// 消息队列存储缓存表
private final ConcurrentMap<String/* topic */, ConcurrentMap<Integer/* queueId */, ConsumeQueue>> consumeQueueTable;
// 消息队列文件 ComsumeQueue 刷盘线程
private final FlushConsumeQueueService flushConsumeQueueService;
// 清除 CommitLog 文件服务
private final CleanCommitLogService cleanCommitLogService;
// 清除 ConsumeQueue 文件服务
private final CleanConsumeQueueService cleanConsumeQueueService;
// 索引文件实现类
private final IndexService indexService;
// MappedFile 分配服务
private final AllocateMappedFileService allocateMappedFileService;
// CommitLog 消息分发,根据 CommitLog 文件构建 ConsumeQueue、IndexFile 文件
private final ReputMessageService reputMessageService;
// 存储 HA 机制
private final HAService haService;
// 消息堆内存缓存
private final TransientStorePool transientStorePool;
// 消息拉取长轮询模式消息达到监听器
private final MessageArrivingListener messageArrivingListener;
// broker 配置属性
private final BrokerConfig brokerConfig;
// 文件刷盘检测点
private StoreCheckpoint storeCheckpoint;
// CommitLog 文件转发请求
private final LinkedList<CommitLogDispatcher> dispatcherList;
消息发送存储流程
代码入口: DefaultMessageStore#putMessage
- 对消息的一些检查,如 当前如果是 SLAVE 则不写入。
- 如果当前延迟等级大于 0,那么会 将该消息放入 SCHEDULE_TOPIC_XXXX 主题中,队列为 延迟等级 - 1
CommitLog#putMessage
// 延迟等级大于 0
if (msg.getDelayTimeLevel() > 0) {
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
topic = ScheduleMessageService.SCHEDULE_TOPIC;
// 这个方法,将 延迟等级 - 1
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
// Backup real topic, queueId
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
msg.setTopic(topic);
msg.setQueueId(queueId);
}
}
- 每个 commitlog 大小为 1G,用第一个偏移量作为文件名;MappedFileQueue 可以看做是 ${ROCKET_HOME}/store/commitlog 目录,MappedFile 则是 文件;在写 commitlog 之前,会写获取锁,并追加写文件。
- 创建全局唯一消息 ID,消息有 16 个字节
- 计算消息长度
代码入口:CommitLog#calMsgLength
protected static int calMsgLength(int sysFlag, int bodyLength, int topicLength, int propertiesLength) {
int bornhostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20;
int storehostAddressLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 8 : 20;
final int msgLen = 4 //TOTALSIZE
+ 4 //MAGICCODE
+ 4 //BODYCRC
+ 4 //QUEUEID
+ 4 //FLAG
+ 8 //QUEUEOFFSET
+ 8 //PHYSICALOFFSET
+ 4 //SYSFLAG
+ 8 //BORNTIMESTAMP
+ bornhostLength //BORNHOST
+ 8 //STORETIMESTAMP
+ storehostAddressLength //STOREHOSTADDRESS
+ 4 //RECONSUMETIMES
+ 8 //Prepared Transaction Offset
+ 4 + (bodyLength > 0 ? bodyLength : 0) //BODY
+ 1 + topicLength //TOPIC
+ 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength
+ 0;
return msgLen;
}
- 计算长度,是为了根据 commitlog 当前的空闲空间,判断是否要重新创建。 如果 消息长度 + 8 大于 commitlog 空闲空间,则重新创建新的 CommitLog。高4节存储当前文件剩余空间,低4字节存储魔数
- 将消息写到 ByteBuffer 中,创建 AppendMessageResult 。只是将消息存储到 MappedFile 映射的 ByteBuffer 中,没有刷盘。
DefaultAppendMessageCallback#doAppend
final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
// Write messages to the queue buffer
byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);
AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId, msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
- 根据刷盘策略,进行同步刷盘或者异步刷盘,序列化消息到磁盘。执行 HA 主从复制
CommitLog#putMessage
handleDiskFlush(result, putMessageResult, msg);
handleHA(result, putMessageResult, msg);
存储文件组织与内存映射
MappedFileQueue 映射文件队列
// 存储目录
private final String storePath;
// 单个文件的存储大小
private final int mappedFileSize;
// MappedFile 文件集合
private final CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<MappedFile>();
// 创建 MappedFile 服务类
private final AllocateMappedFileService allocateMappedFileService;
// 当前刷盘指针,指针之前数据已完成刷盘
private long flushedWhere = 0;
// 当前数据提交指针,内存中 ByteBuffer 当前的写指针,该值大于等于 flushedWhere
private long committedWhere = 0;
根据 时间戳 查找 MappedFile
public MappedFile getMappedFileByTime(final long timestamp) {
Object[] mfs = this.copyMappedFiles(0);
if (null == mfs)
return null;
for (int i = 0; i < mfs.length; i++) {
MappedFile mappedFile = (MappedFile) mfs[i];
if (mappedFile.getLastModifiedTimestamp() >= timestamp) {
return mappedFile;
}
}
return (MappedFile) mfs[mfs.length - 1];
}
流程:从 MappedFile 列表的第一个开始查找,找到第一个最后一次更新时间大于 timestamp 的文件,不存在,则返回最后一个文件。
根据 offset 查找 MappedFile
public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
try {
MappedFile firstMappedFile = this.getFirstMappedFile();
MappedFile lastMappedFile = this.getLastMappedFile();
if (firstMappedFile != null && lastMappedFile != null) {
...
...
// 因为 第一个 MappedFile 偏移量可能不是 0 (文件定期删除机制导致)。因此采用此算法。
int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
MappedFile targetFile = null;
try {
targetFile = this.mappedFiles.get(index);
} catch (Exception ignored) {
}
// 找到的文件合法,直接返回
if (targetFile != null && offset >= targetFile.getFileFromOffset()
&& offset < targetFile.getFileFromOffset() + this.mappedFileSize) {
return targetFile;
}
// 遍历所有 MappedFiles,如果 offset 在 MappedFile 的 起始区间,则返回
for (MappedFile tmpMappedFile : this.mappedFiles) {
if (offset >= tmpMappedFile.getFileFromOffset()
&& offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) {
return tmpMappedFile;
}
}
}
// 未找到 直接返回第一个文件
if (returnFirstOnNotFound) {
return firstMappedFile;
}
}
} catch (Exception e) {
log.error("findMappedFileByOffset Exception", e);
}
return null;
}
-
根据算法
int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
查找 MappedFile。
- 如果 index 查找的 MappedFile 无效。则遍历所有的 MappedFile,如果 offset 落在 MappedFile 的起始区间内。则直接返回
- 如果还是找不到,返回第一个文件
MappedFile 内存映射文件
// 操作系统页大小
public static final int OS_PAGE_SIZE = 1024 * 4;
// 当前 JVM 实例中 MappedFile 虚拟内存
private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);
// 当前 JVM 实例中 MappedFile 个数
private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
// 当前该文件写指针,从 0 开始(内存映射中的写指针)
protected final AtomicInteger wrotePosition = new AtomicInteger(0);
// 当前文件的提交指针,如果开启 transientStorePoolEnable, 则数据会存储在 TransientStorePool 中,然后提交到内存映射 ByteBuffer 中,再刷写到磁盘
protected final AtomicInteger committedPosition = new AtomicInteger(0);
// 刷写到磁盘指针,该指针之前的数据持久化到磁盘中
private final AtomicInteger flushedPosition = new AtomicInteger(0);
// 文件大小
protected int fileSize;
// 文件通道
protected FileChannel fileChannel;
// 堆内存 ByteBuffer,如果不为空,数据首先将存储在该 buffer,然后提交到 MappedFile 对应的内存映射文件 Buffer。transientStorePoolEnable 为 true 时不为空。
protected ByteBuffer writeBuffer = null;
// 堆内存池,transientStorePoolEnable 为 true 时启动。
protected TransientStorePool transientStorePool = null;
// 文件名称
private String fileName;
// 该文件的初始偏移量
private long fileFromOffset;
// 物理文件
private File file;
// 物理文件对应的内存映射 Buffer
private MappedByteBuffer mappedByteBuffer;
// 文件最后一次写入时间
private volatile long storeTimestamp = 0;
// 是否是 MappedFileQueue 中第一个文件
private boolean firstCreateInQueue = false;
MappedFile 初始化
根据是否开启 transientStorePoolEnable 存在两种初始化情况。 transientStorePoolEnable 为 true 表示内存先存储在堆外内存,然后通过 Commit 线程将数据提交到内存映射 MappedByteBuffer 中,再通过 Flush 线程将内存映射 Buffer 中的数据持久化到磁盘中。
MappedFile#init 逻辑简单,不贴代码了。创建文件,将文件内容映射到 Buffer。
如果 transientStorePoolEnable 为 true,则初始化 MappedFile 的 writeBuffer。
MappedFile 提交
MappedFile#commit
public int commit(final int commitLeastPages) {
// transientStorePoolEnable= true,该值不为null
if (writeBuffer == null) {
return this.wrotePosition.get();
}
// 是否可以被提交
if (this.isAbleToCommit(commitLeastPages)) {
if (this.hold()) {
commit0(commitLeastPages);
this.release();
} else {
log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
}
}
// All dirty data has been committed to FileChannel.
if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
this.transientStorePool.returnBuffer(writeBuffer);
this.writeBuffer = null;
}
return this.committedPosition.get();
}
// 执行提交逻辑,所以,其实所谓的提交,只是设置了 committedPosition 的位置,因为 MappedFile 还有个刷盘机制
protected void commit0(final int commitLeastPages) {
int writePos = this.wrotePosition.get();
int lastCommittedPosition = this.committedPosition.get();
if (writePos - this.committedPosition.get() > 0) {
try {
// 共享数据,但是各自维护一套 position,mark,limit
ByteBuffer byteBuffer = writeBuffer.slice();
byteBuffer.position(lastCommittedPosition);
byteBuffer.limit(writePos);
this.fileChannel.position(lastCommittedPosition);
this.fileChannel.write(byteBuffer);
this.committedPosition.set(writePos);
} catch (Throwable e) {
log.error("Error occurred when commit data to FileChannel.", e);
}
}
}
MappedFile 刷盘
刷盘指将内存中的数据刷写到磁盘。这里有专门的刷盘线程在处理刷盘, 具体可查看 FlushCommitLogService,异步刷盘时,真正调用刷盘的类是:CommitRealTimeService,同步刷盘则由 GroupCommitService
MappedFile#flush
public int flush(final int flushLeastPages) {
if (this.isAbleToFlush(flushLeastPages)) {
if (this.hold()) {
int value = getReadPosition();
try {
//We only append data to fileChannel or mappedByteBuffer, never both.
if (writeBuffer != null || this.fileChannel.position() != 0) {
// 刷盘
this.fileChannel.force(false);
} else {
// 刷盘
this.mappedByteBuffer.force();
}
} catch (Throwable e) {
log.error("Error occurred when force data to disk.", e);
}
this.flushedPosition.set(value);
this.release();
} else {
log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
this.flushedPosition.set(getReadPosition());
}
}
return this.getFlushedPosition();
}
MappedFile 销毁
MappedFile#destroy
public void shutdown(final long intervalForcibly) {
// 第一次调用一定是 true
if (this.available) {
this.available = false;
this.firstShutdownTimestamp = System.currentTimeMillis();
// 尝试释放资源
this.release();
} else if (this.getRefCount() > 0) {
// RocketMQ 有 最大拒绝存活时间概念。只要超过了,会对资源的 引用 -100。再尝试释放资源
if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) {
this.refCount.set(-1000 - this.getRefCount());
this.release();
}
}
}
// 资源释放
public void release() {
long value = this.refCount.decrementAndGet();
if (value > 0)
return;
synchronized (this) {
// 将 cleanupOver 设置为 true, 判断是否清理完毕的判断参数。
this.cleanupOver = this.cleanup(value);
}
}
// 判断是否已经被清理干净
public boolean isCleanupOver() {
return this.refCount.get() <= 0 && this.cleanupOver;
}
public boolean cleanup(final long currentRef) {
if (this.isAvailable()) {
log.error("this file[REF:" + currentRef + "] " + this.fileName
+ " have not shutdown, stop unmapping.");
return false;
}
if (this.isCleanupOver()) {
log.error("this file[REF:" + currentRef + "] " + this.fileName
+ " have cleanup, do not do it again.");
return true;
}
clean(this.mappedByteBuffer);
// 维护 总文件大小
TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(this.fileSize * (-1));
// 维护 文件个数
TOTAL_MAPPED_FILES.decrementAndGet();
log.info("unmap file[REF:" + currentRef + "] " + this.fileName + " OK");
return true;
}
TransientStorePool
// availableBuffers 大小,默认5个。 broker 配置文件设置 transientStorePoolSize
private final int poolSize;
// ByteBuffer 大小。默认为 mappedFileSizeCommitLog.表明 TransientStorePoolSize 为 commitlog 文件服务。
private final int fileSize;
// 双端队列
private final Deque<ByteBuffer> availableBuffers;
RocketMQ 存储文件
文件介绍 ${ROCKET_HOME}/store
- commitlog:消息存储目录
-
config:运行期间一些配置信息,主要包括以下信息
- consumerFilter.json:主题消息过滤信息
- consumerOffset.json:集群消费模式消息消费进度
- delayOffset.json:延迟消息队列拉取进度
- subscriptionGroup.json:消息消费组配置信息
- topic.json:topic 配置属性
- consumequeue:消息消费队列目录
- index: 消息索引文件存储目录
- abort:存在 abort 文件说明 broker 非正常关闭,默认启动时创建,正常退出之前删除
- checkpoint:文件检测点,存储 commitlog 文件最后一次刷盘时间戳、consumequeue 最后一次刷盘时间、index 索引文件最后一次刷盘时间戳
commitlog
消息查找
CommitLog#getMessage()
public SelectMappedBufferResult getMessage(final long offset, final int size) {
// 默认是 1G 1024 * 1024 * 1024
int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog();
// 根据 offset 找到 MappedFile
MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0);
if (mappedFile != null) {
// 根据 pos(消息起点),size(消息长度) 从 ByteBuffer 中获取消息
int pos = (int) (offset % mappedFileSize);
return mappedFile.selectMappedBuffer(pos, size);
}
return null;
}
// MappedFile#selectMappedBuffer()
public SelectMappedBufferResult selectMappedBuffer(int pos, int size) {
int readPosition = getReadPosition();
if ((pos + size) <= readPosition) {
if (this.hold()) {
// 共享内存,但各自保存 pos,mark,limit 指针
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
byteBuffer.position(pos);
ByteBuffer byteBufferNew = byteBuffer.slice();
byteBufferNew.limit(size);
return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
} else {
log.warn("matched, but hold failed, request pos: " + pos + ", fileFromOffset: "
+ this.fileFromOffset);
}
} else {
log.warn("selectMappedBuffer request pos invalid, request pos: " + pos + ", size: " + size
+ ", fileFromOffset: " + this.fileFromOffset);
}
return null;
}
consumequeue 文件
consumequeue 目录下的每个文件夹,代表一个 主题。二级目录,则是该主题下的队列(默认4个)。
Consumequeue条目,存储格式
consumeQueue 默认包含 30W 个条目,单个文件长度 30W * 20 字节 = 5 .7MB
根据 offset 查找消息
public SelectMappedBufferResult getIndexBuffer(final long startIndex) {
// 600w 5.7mb
int mappedFileSize = this.mappedFileSize;
// 获取文件物理偏移量
long offset = startIndex * CQ_STORE_UNIT_SIZE;
if (offset >= this.getMinLogicOffset()) {
// ? 为什么 这里可以根据 offset 找到对应的 MappedFile
MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset);
if (mappedFile != null) {
SelectMappedBufferResult result = mappedFile.selectMappedBuffer((int) (offset % mappedFileSize));
return result;
}
}
return null;
}
根据时间戳查找消息
public long getOffsetInQueueByTime(final long timestamp) {
// 定位 MappedFile
MappedFile mappedFile = this.mappedFileQueue.getMappedFileByTime(timestamp);
...
...
long minPhysicOffset = this.defaultMessageStore.getMinPhyOffset();
...
...
// 二分查找法
while (high >= low) {
midOffset = (low + high) / (2 * CQ_STORE_UNIT_SIZE) * CQ_STORE_UNIT_SIZE;
byteBuffer.position(midOffset);
// 记住 consumequeue 条目的数据结构, 前8个字节是 commitlog offset 偏移量
long phyOffset = byteBuffer.getLong();
// 获取消息大小, 4个字节
int size = byteBuffer.getInt();
// 物理偏移量 小于 最新的物理偏移量,很明显这是一个无效的消息。
if (phyOffset < minPhysicOffset) {
low = midOffset + CQ_STORE_UNIT_SIZE;
leftOffset = midOffset;
continue;
}
long storeTime =
this.defaultMessageStore.getCommitLog().pickupStoreTimestamp(phyOffset, size);
if (storeTime < 0) {
return 0;
} else if (storeTime == timestamp) {
targetOffset = midOffset;
break;
} else if (storeTime > timestamp) {
high = midOffset - CQ_STORE_UNIT_SIZE;
rightOffset = midOffset;
rightIndexValue = storeTime;
} else {
low = midOffset + CQ_STORE_UNIT_SIZE;
leftOffset = midOffset;
leftIndexValue = storeTime;
}
}
...
...
}
查找消息的大体流程都是:先找到 MappedFile,在从 consumequeue 中获取 offset,然后根据 offset 到 MappedFile 获取消息
index 索引文件(为消息建立索引)
-
IndexHead(共 40 个字节)
- beginTimestamp:索引文件包含消息的最小存储时间
- endTimestamp:包含消息的最大存储时间
- beginPhyoffset:包含消息的最小偏移量(commitlog文件偏移量)
- endphyoffset:包含消息的最大物理偏移量(commitlog文件偏移量)
- Hashslotcount:hashlot 个数
- indexcount:Index 条目列表当前已使用个数
- Hash 槽
IndexFile 默认有 500w 个 Hash槽,每个 Hash槽存储的是落在该 Hash 槽的 hashcode 最新的 index 下标。
-
Index条目列表
- Hashcode:key 的 hashcode
- Phyoffset:消息对应的物理偏移量
- Timedif:该消息存储时间与第一条消息的时间戳差值,小于0该消息无效
- preIndexNo:该条目的前一条 index 索引,当出现 hash 冲突时,构建的链表结构
消息索引写入
IndexFile#putKey()
// key:消息key ; phyoffset:消息物理偏移量;storeTimestamp:消息存储时间
public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
// 小于 最大 index 条目数量 2000W
if (this.indexHeader.getIndexCount() < this.indexNum) {
// 将 key hash
int keyHash = indexKeyHashMethod(key);
// keyHash % 500w
int slotPos = keyHash % this.hashSlotNum;
// 加上 indexHead 大小(20) 得到 hash槽 物理位置
int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
FileLock fileLock = null;
try {
int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
slotValue = invalidIndex;
}
// 获取 消息存储时间与第一条消息的时间戳差值(秒)
long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
timeDiff = timeDiff / 1000;
if (this.indexHeader.getBeginTimestamp() <= 0) {
timeDiff = 0;
} else if (timeDiff > Integer.MAX_VALUE) {
timeDiff = Integer.MAX_VALUE;
} else if (timeDiff < 0) {
timeDiff = 0;
}
// 写入 index 条目
int absIndexPos =
IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
+ this.indexHeader.getIndexCount() * indexSize;
this.mappedByteBuffer.putInt(absIndexPos, keyHash);
this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
// 往 hash 槽中写入 当前最新 index 条目下标
this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
// 更新 indexHeader 信息
if (this.indexHeader.getIndexCount() <= 1) {
this.indexHeader.setBeginPhyOffset(phyOffset);
this.indexHeader.setBeginTimestamp(storeTimestamp);
}
this.indexHeader.incHashSlotCount();
this.indexHeader.incIndexCount();
this.indexHeader.setEndPhyOffset(phyOffset);
this.indexHeader.setEndTimestamp(storeTimestamp);
// ....
}
消息查找
IndexFile#selectPhyOffset
代码比较简单,不贴了。流程大致如下:
根据偏移量,从 hash 槽中,找到对应的下标。 可能会有 hash 冲突,因此,还得根据 pre index no 查找上一个消息。知道找到为止。
checkpoint 文件
记录,commitlog,consumequeue,index 文件的刷盘点
- pyysicMsgTimestamp:commitlog 文件刷盘时间点
- logicMsgTimestamp:消息消费队列刷盘时间点
- indexMsgTimestamp:index 刷盘时间点
实时更新消息消费队列与索引文件
当消息存放到 commitlog 时,ConsumeQueue,IndexFile 需要及时更新。否则消息无法被消费。通过 ReputMessageService 准实时转发 CommitLog 文件更新事件,由相应的任务处理器根据转发的消息及时更新 ConsumeQueue、IndexFile 文件。
更新 consumequeue
DefaultMessageStore 在启动时,会让 reputmesageService 先确保完成发送落后的消息。落后的消息是指, commitlog 的偏移量 落后于 comsumequeue 的最大偏移量。
DefaultMessageStore#start()
{
// 获取消息的最大的物理偏移量
long maxPhysicalPosInLogicQueue = commitLog.getMinOffset();
for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {
for (ConsumeQueue logic : maps.values()) {
if (logic.getMaxPhysicOffset() > maxPhysicalPosInLogicQueue) {
maxPhysicalPosInLogicQueue = logic.getMaxPhysicOffset();
}
}
}
// ...
// ...
// 启动 reputMessageService,完成 commitlog 消息转发
this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
this.reputMessageService.start();
// 等待完成发送落后消息
while (true) {
if (dispatchBehindBytes() <= 0) {
break;
}
Thread.sleep(1000);
}
}
ReputMessageService 启动时,每次任务推送休息 1 毫秒就继续推送消息。因此是准实时的,即基本可以理解为,只要 commitlog 文件被写入, consumequeue,index 文件就会立即被更新,消费者也能及时消费到消息
ReputMessageService#run()
public void run() {
while (!this.isStopped()) {
try {
Thread.sleep(1);
// 转发
this.doReput();
} catch (Exception e) {
DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
}
ReputMessageService#doReput()
从 commitlog 中获取消息。一条一条的进行转发。将消息封装 DispatchRequest,由 CommitLogDispatcher 转发器对 DispatchRequest 进行转发。 由 CommitLogDispatcherBuildConsumeQueue 处理转发 consumequeue ,CommitLogDispatcherBuildIndex 处理转发 indexFile。
private void doReput() {
for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
// 获取可转发的数据
SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
if (result != null) {
try {
this.reputFromOffset = result.getStartOffset();
// 一条条转发
for (int readSize = 0; readSize < result.getSize() && doNext; ) {
// 将消息封装为 DispatchRequest
DispatchRequest dispatchRequest = DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
if (dispatchRequest.isSuccess()) {
if (size > 0) {
// ...
// 转发
DefaultMessageStore.this.doDispatch(dispatchRequest);
this.reputFromOffset += size;
readSize += size;
// ...
}
}
}
}
}
}
CommitLogDispatcherBuildConsumeQueue 处理转发时,交由 DefaultMessageStore#putMessagePositionInfo() 方法进行转发。
根据主题和队列ID 找到对应的 队列。最终由 ConsumeQueue 自身处理消息
public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
cq.putMessagePositionInfoWrapper(dispatchRequest);
}
ConsumeQueue 在处理消息时,只完成了在内存中对消息的追加
ConsumeQueue#putMessagePositionInfo
// ...
// 消息追加
this.byteBufferIndex.flip();
this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
this.byteBufferIndex.putLong(offset);
this.byteBufferIndex.putInt(size);
this.byteBufferIndex.putLong(tagsCode);
// ...
mappedFile.appendMessage(this.byteBufferIndex.array());
更新 index
index 的转发任务实现类:CommitLogDispatcherBuildIndex
如果 messageIndexEnable,则构建 索引。
如果唯一键,不为空,则构建。RocketMQ 支持为同一个消息构建多个索引,使用 空格 分割。
消息队列与索引文件恢复
加载 commitlog,consumeQueue,indexFile。rocketMQ 通过 abort 文件,判断 broker 是否正常退出。
如果是正常退出,那么 从 MappedFile 的倒数第 3 个文件开始加载,如果不足,则从第1个开始。找到最后一个 偏移量。 并用之设置 MappedFileQueue 的 flushedPosition,commitPosition。
如果非正常退出,则由后往前找到最后一个有效的文件,并将之所有消息 转发给 comsumequeue、indexFile。用最后一个有效文件的最后一个消息的偏移量 设置 MappedFileQueue 的 flushedPosition,commitPosition。因为重新 commitlog 消息,因为会存在 消息重复消费问题。
最后, rocketMQ 都会删掉 commitlog , consumequeue, indexFile 的脏数据。
代码位置:DefaultMessageStore#load()
文件刷盘机制
代码位置:Commitlog#handleDiskFlush
同步刷盘
将刷盘任务封装为 GroupCommitRequest,由 GroupCommitService 每隔 10 毫秒 对一批消息进行刷盘。默认 5 秒 内,没完成刷盘动作,则视为刷盘失败。
真正执行刷盘,则是由 MappedFile#flush 做刷盘动作。因此,如果是同步刷盘操作的话,不可能出现消息丢失
异步刷盘
异步刷盘分 2 种情况,
transientStorePoolEnable = true
- 消息直接追加到 ByteBuffer,wrotePosition 不断追加
- CommitRealTimeService 线程默认每 200ms 将 ByteBuffer 新追加的内容(wrotePosition - commitPosition) 写到 MappedByteBuffer。 commitPosition 向前移动到本次提交内容的长度,wrotePosition 继续向前。
- FlushRealTimeService 线程默认 每 500ms 将 MappedByteBuffer 刷写到磁盘。
transientStorePoolEnable = false
由 FlushRealTimeService 线程进行刷盘
过期文件删除
如果非当前写文件在一定时间间隔内没有再次被更新,则认为是过期文件,可以被删除。RocketMQ 不会关注这个文件上的消息是否全部被消费。默认每个文件的过期时间为72 个小时。默认每隔 10 秒扫描过期文件
代码位置: DefaultMessageStore#addScheduleTask
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。