整体流程
Prodecer生产的消息通过Netty传输到Broker,由Broker模块的类SendMessageProcessor的processRequest的方法来处理消息。本文会把具体的类名、方法名列出来,方便Debug源码。
1.写消息到内存
- 根据请求码判断消息的类型。是重试消费的消息,还是新消息。
- 根据请求生成消息的实体类对象MessageExtBrokerInner
- 获取缓存队列MappedFileQueue中最后的MappedFile,如果获取不到,则新建一个。这里的MappedFile是指CommitLog,也就是我们需要顺序写的文件,最终所有的消息都会顺序写到CommitLog中。这里的写是顺序写的,消息始终追加到CommitLog的最后。MapperFile和物理文件是一一对应的关系。
- 对写入消息的对象加锁,防止多线程写的异常,该锁默认使用CAS自旋获取,锁类为PutMessageLock。将消息体写入到内存中。考虑到消费消息如果都依赖于CommitLog,消费速度会很慢,因此会根据CommitLog生成对应的消费队列索引文件ConsumeQueue。CommitLog的数据结构见下图:
其中physicaloffset 是指该消息的物理offset,即图中的 wroteOffset, 它等于 fileFromOffset(当前mappedFile的物理offset) 加上 mappedFile 对应的 buffer 的 position(一个逻辑的offset). - 解锁。
2.刷盘
根据刷盘策略,分为同步刷盘和异步刷盘:
- 同步刷盘需要开启一个线程GroupCommitRequest,GroupCommitRequest封装了一个等待线程大小为1的CountDownLatch,交给GroupCommitService线程去处理刷盘操作,并且等待刷盘完成。这里另起一个线程,可以不影响其他线程的消息写入到内存中。
- 异步刷盘用 countDownLatch notify 负责刷盘的线程后就直接返回了。
3.HA
HA也就是指将 主机的消息同步到从机。这里也分为同步和异步,在broker的配置文件中可以配置复制方式。这里同样是将GroupCommitRequest交给HAService去处理,同步需要主线程的GroupCommitRequest等待从机返回,这里用的JDK自带的NIO。刷盘、同步完成后,会将结果反馈给Producer。
4.生成索引文件
上文提到如果仅靠CommitLog消费消息,会存在读取效率低的问题。因此RocketMq会根据配置文件(无配置则使用默认配置)生成对应数量的消费队列索引。 ReputMessageService是由Broker启动开启的线程。自旋检查,如果检测到有消息写入CommitLog,立即开始消息同步操作,代码如下:
private boolean isCommitLogAvailable() {
return this.reputFromOffset < DefaultMessageStore.this.commitLog.getMaxOffset();
}
private void doReput() {
for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
&& this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
break;
}
SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
if (result != null) {
try {
this.reputFromOffset = result.getStartOffset();
for (int readSize = 0; readSize < result.getSize() && doNext; ) {
-------
}
- 关于ConsemeQueue,主要是将CommitLog中的 physicaloffset、size、Tag hashCode根据CommitLog对应的QueueID写入到对应的Queue中。系统可以根据这些消息,定位出CommitLog中的一条消息。
- 关于 Index,这是类似于HashMap的数据结构,有500W个bucket。根据topic+UNIQUE_KEY 求HASH 值写入到对应的位置。先计算absIndexPox,再从absIndexPos写入一系列的值。实际上就是HashMap序列化的过程,这样根据topic 和 key 找到 IndexFile 中的一条记录,根据其中的 commitLog offset 去读取真正的消息。 //TODO 序列化逻辑待补
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);
this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());