RocketMQ源码解析-消息是如何写入Broker服务器(客户端篇)
RocketMQ源码解析-消息是如何写入Broker服务器(Broker端篇)
经过前面两篇的梳理,我们已经知道了一条普通的消息是如何由客户端构造,进而发送到服务端,经过服务端处理后,最终写入到了消息的目的地commitlog文件中。
但是在RocketMQ的发布订阅模型中,消息者获取消息的首要目标是ConsumeQueue,ConsumeQueue是一个逻辑上面的概念,其本质也是基于一组MappedFile来实现的。本篇文章,我们来梳理一下RocketMQ是如何将写入commitlog文件的消息继续追加到ConsumeQueue中。
为了让读者从概念上更好的理解本篇的意图,借鉴了一张RocktMQ官方提供的设计图。本篇主要讲解蓝实线部分的实现细节,从源码入手,深入掌握。
首先明确一点,RocktMQ将消息写入commitlog的线程与ConsumeQueue的线程不是同一个,ConsumeQueue是一个异步构造的过程,写入commitlog的线程是由sendMessageExecutor这个线程池提供的,而写入ConsumeQueue的线程是由ReputMessageService这个服务类提供的。
这个类继承自ServiceThread,ServiceThread又继承自Runnable。ServiceThread类包含一个thread的属性用于创建线程执行run方法。
ReputMessageService类有一个属性是reputFromOffset,这个属性代表的是将会从哪个字节偏移量开始转发commitlog中的消息到对应的ConsumeQueue中。
DefaultMessageStore类的start方法中,涉及到了ReputMessageService线程的启动。
在ReputMessageService线程启动之前,明确了其属性reputFromOffset的值应该是多少。
在服务正常的执行流程下,reputFromOffset的取值会是所有ConsumeQueue中最大字节偏移量。
每个ConsumeQueue中都会有一个属性maxPhysicOffset来表示本ConsumeQueue中存储的commitlog最大字节偏移量。
将ReputMessageService的reputFromOffset属性设置完毕,并调用start方法后,ReputMessageService服务开始运转了起来,run方法就是线程要执行的目标方法。
可以看到run方法的内部是一个while的无限循环,根据变量stopped来进行控制,然后每隔1毫秒执行一次doReput方法,doReput方法是消息由commitlog转发至ConsumeQueue的核心处理。
doReput方法的核心逻辑就是根据reputFromOffset从commitlog中获取待转发的字节数据,然后调用checkMessageAndReturnSize将读取出来的字节数据反序列化为一条完整的写入消息,进行封装成DispatchRequest实例,然后将DispatchRequest作为参数分别调用consumeQueue和indexfile提供的写入方法,来完成consumeQueue和indexfile的写入,循环往复的执行。
接下来我们拆解一下doReput方法中比较重要的几个核心方法,首先是commitLog.getData(reputFromOffset),这个方法的作用就是根据reputFromOffset表示的字节偏移量,来获取要进行转发的字节数据。
这个方法内部主要分为了两步,第一步是根据reputFromOffset来确定mappedFile,也就是commitlog,第二步就是在确定了mappedFile的前提下,根据取余算法来确定reputFromOffset在mappedFile中的相对位置pos,继而根据pos以及当前这个mappedFile中最大可读字节偏移量的位置来返回要转发的字节数据。
DefaultMessageStore.this.doDispatch(dispatchRequest)这个方法就是将消息转发到consumeQueue和indexfile的核心方法。
dispatcherList是一个集合,在DefaultMessageStore的构造方法中将处理consumeQueue和indexfile的实例放入进去。本篇着重分析的是CommitLogDispatcherBuildConsumeQueue。
CommitLogDispatcherBuildConsumeQueue的入口方法dispatch,只转发普通类型以及事务提交的消息,然后根据topic以及queueId这两个属性获取到逻辑上的ConsumeQueue,进而调用putMessagePositionInfoWrapper将消息写入。
将DispatchRequest封装的消息内容写入到consume中,首先校验了一下当前RocketMQ存储模块的状态是否支持可以写入,然后才执行后续的写入逻辑,允许写入失败,并且最大重复30次。
调用putMessagePositionInfo方法,将DispatchRequest封装的消息在commitlog的偏移量、消息的大小msgSize、消息过滤tagCode、consumeQueueOffset作为参数传入。在ConsumeQueue中,存储的不是一条完整的消息,而是存储的是消息在commitlog中的偏移量、消息大小、tagCode数据,并且是以固定20字节的长度存储。consumeQueueOffset是用来计算本条消息要写入字节数据的开始偏移量位置。
putMessagePositionInfo方法的核心处理逻辑,将消息的在commitlog的物理偏移量offset、消息大小size、tagsCode写入byteBufferIndex,然后调用mappedFile.appendMessage方法将消息追加到mappedFile中去。在此处一个mappedFile表示一个consumeQueue的物理文件。
在mappedFile.appendMessage方法中,消息是直接追加到MappedByteBuffer中去,此处跟commitlog的写入方式不同,commitlog写入或许写入堆外内存或许写入MappedByteBuffer,需要根据条件进行判断。
这个方法结束后,代表一条消息已经写入到对应的consumeQueue中了,后续消费者就可以根据对应的队列来获取这条消息进行消费了。
梳理完本篇文章之后,我们了解到消息在写入commitlog之后是如何继续写入到consumeQueue的。普通消息在写入到consumeQueue后,这条消息就对消费者可见了,消费者就可以获取到这条消息进而完成后续的业务逻辑。
dispatcherList另一个实例是CommitLogDispatcherBuildIndex,这个是构建indexfile文件的处理器,indexfile文件是消息的索引文件,内部仿照HashMap的数据结构来构建,后续我们来梳理一下这个文件的内容。
我们后续再见,谢谢大家。