RocketMQ 消息存储机制

1,325 阅读3分钟

整体流程

Prodecer生产的消息通过Netty传输到Broker,由Broker模块的类SendMessageProcessorprocessRequest的方法来处理消息。本文会把具体的类名、方法名列出来,方便Debug源码。

1.写消息到内存

  1. 根据请求码判断消息的类型。是重试消费的消息,还是新消息。
  2. 根据请求生成消息的实体类对象MessageExtBrokerInner
  3. 获取缓存队列MappedFileQueue中最后的MappedFile,如果获取不到,则新建一个。这里的MappedFile是指CommitLog,也就是我们需要顺序写的文件,最终所有的消息都会顺序写到CommitLog中。这里的写是顺序写的,消息始终追加到CommitLog的最后。MapperFile和物理文件是一一对应的关系。
  4. 对写入消息的对象加锁,防止多线程写的异常,该锁默认使用CAS自旋获取,锁类为PutMessageLock。将消息体写入到内存中。考虑到消费消息如果都依赖于CommitLog,消费速度会很慢,因此会根据CommitLog生成对应的消费队列索引文件ConsumeQueue。CommitLog的数据结构见下图:
    其中physicaloffset 是指该消息的物理offset,即图中的 wroteOffset, 它等于 fileFromOffset(当前mappedFile的物理offset) 加上 mappedFile 对应的 buffer 的 position(一个逻辑的offset).
  5. 解锁。

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; ) {
                        -------
                        }
  1. 关于ConsemeQueue,主要是将CommitLog中的 physicaloffset、size、Tag hashCode根据CommitLog对应的QueueID写入到对应的Queue中。系统可以根据这些消息,定位出CommitLog中的一条消息。
  2. 关于 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());