“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第4篇文章,点击查看活动详情”
往期回顾
RocketMQ源码解析-消息是如何写入Broker服务器(客户端篇)
RocketMQ源码解析-消息是如何写入Broker服务器(Broker端篇)
RocketMQ源码解析-步步拆解ConsumeQueue的写入流程
RocketMQ消息写入-步步拆解IndexFile的写入流程
关于RocketMQ消息消息的文章,我们已经梳理了两篇,重点介绍了消息消费前的准备工作以及客户端是如何串接消息拉取各个环节的。这两篇文章梳理完后,接下来我们重点介绍一下在客户端向服务器发起消息拉取的请求后,服务端是如何响应客户端拉取请求的,在这个过程中,服务端做了哪些事情,有哪些是值得我们学习和借鉴的,在接下来的篇幅中会详细的介绍。
重点脉络梳理
本示例图展示了在客户端向服务端发送消息拉取的请求后,该服务请求在服务端处理过程中涉及的主要服务处理类,主要处理流程如下:
1.由RocketMQ客户端构建PullMessageRequestHeader消息拉取请求头,消息拉取请求头中封装了本次请求涉及的几个重要的参数,这几个参数如下
PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();
requestHeader.setConsumerGroup(this.consumerGroup);//组名
requestHeader.setTopic(mq.getTopic());//topic
requestHeader.setQueueId(mq.getQueueId()); //队列
requestHeader.setQueueOffset(offset);//本次开始拉取的偏移量
requestHeader.setMaxMsgNums(maxNums);//能够拉取的最大消息数量
requestHeader.setSysFlag(sysFlagInner);//系统标记
requestHeader.setCommitOffset(commitOffset);//待提交的本地偏移量
requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);//长轮询消息拉取挂起时间
requestHeader.setSubscription(subExpression);//过滤表达式
requestHeader.setSubVersion(subVersion);//订阅版本信息
requestHeader.setExpressionType(expressionType);//消息过滤表达式类型
服务端就是根据这几个参数来查找消息继而返回给客户端。
2.由MQClientAPIImpl发起对服务端的请求,服务接收到新的请求后,根据RequestCode解析出来处理本次请求的处理器。
3.本次请求交由PullMessageProcessor来进行处理,这个处理器在做了一些前置的校验后,会根据Topic和QueueId找到逻辑对象ConsumeQueue,然后根据Offset继续定位到具体的物理文件。
4.从ConsumeQueue对应的物理文件中读取索引数据,根据索引的数据可以过滤一些不符合本次拉取请求的消息。
5.根据符合本次消息拉取请求的索引数再,从commitlog中读取真正的消息,解析完后放入到GetMessageResult中,然后封装进PullMessageResponseHeader响应头。
6.将PullMessageResponseHeader放入RemotingCommand中去,返回给客户端。
以上这些就是消息拉取的大概流程。接下来我们站在源码的角度,按照这个流程,去分析一下RocketMQ的实现过程。
源码解析
我们从PullMessageProcessor的processRequest方法开始,这个是服务端处理拉取消息请求的入口方法,这个方法可以分成三个部分来看,分别如下:
第一部分
第一部分是一些前置的校验以及后面要用到的对象实例的初始化。
第二部分
第二部分是开始正式的获取消息。
第三部分
第三部分是获取消息之后的处理。
我觉得有两个点需要关注一下,一个是消息拉取的长轮询是如何实现的,另外一个就是关于transferMsgByHeap的配置,也就是如何写回到客户端,是经由堆内存处理一下在返回还是利用Netty提供的零拷贝技术直接由内核空间transferTo到连接channel。
重点解析
我们来重点分析一下第二部分,也就是看一下服务端是如何获取消息的。
getMessage是DefaultMessageStore类中的方法,这个类还持有commitLog以及consumeQueueTable的引用,消息的获取也主要是commitLog以及consumeQueue的配合来进行。
由以上的源码分析可知,在offset也就是消息拉取索引满足条件的情况下会调用这段逻辑用来从ConsumeQueue中获取待消费消息的索引。
SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset);
我们重点看一这个方法是如何实现的。
其中调用了如下的方法来确认物理偏移量所属的物理文件。
MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset);
这个是findMappedFileByOffset最终的实现方法。我们来看一下如何根据物理偏移量确认最终所属的物理文件。
经由前面的分析在获取到ConsumeQueue中待拉取消息的索引后,根据索引组成部分的物理偏移量(offsetPy)以及消息的大小(sizePy)来调用commitLog的getMessage来获取最终的消息数据。
SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);
实现方式是一样的,都是由偏移量定位物理文件,接着定位到该物理文件的相对位置,获取该相对位置之后的可读数据并返回。
本期结尾
由于篇幅的原因,本期并没有涉及到消息拉取的长轮询实现以及利用Netty零拷贝技术实现消息发送,希望后续可以单独拿出来梳理一下。
关于消息拉取的服务端实现也梳理完毕了,可以看到服务端对于消息拉取的实现还是比较简单的,反而客户端在消息消费上实现的功能就比较多比较重了。
关于消息消费的梳理,接下来就还剩客户端在获取到服务端响应的内容后是如何处理的这一部分了,后续将会解析一下这部分是如何实现的,这样一来,整个消息消费的流程就全部解析完了,希望能够对大家的工作以及学习提供一些帮助。