RocketMQ源码分析之消费者

RocketMQ源码分析我们主要从NameSrv、路由、生产者、消费者、消息存储等方面一点点分析,本章主要讲的是消费者的源码分析。

一、前提

消费者消费分为两种模式,集群模式和广播模式,默认开启的是集群模式,集群模式下同一个消费组中只能有一个消费者消费某个topic在broker中的队列。广播模式下,所有消费者都可以消费topic的信息。

消费者获取消息的方式也有两种,一个是主动从broker中pull消息,另一个是broker主动push消息给消费者。其实broker主动push消息给消费者,说到底还是消费者先发送请求给broker,表明自己需要哪些topic的消息,然后broker定期扫描,然后收到该topic的消息再发送给消费者。

二、消息的拉取

默认使用的是DefaultMQPushConsumer类,处于org.apache.rocketmq.client.consumer包下,该类继承ClientConfig配置文件类实现MQPushConsumer接口,MQPushConsumer接口继承MQConsumer接口,MQConsumer接口继承MQAdmin接口,即DefaultMQPushConsumer--》MQConsumer--》MQAdmin。

通过实现的关系,可以发现无论生产者还是消费最后都是实现了MQAdmin接口,因此MQAdmin接口中都是跟topic相关的方法,MQConsumer接口中就是三个方法,一个是通过topic从消费者本地缓存获取消息队列,另外两个都是消费消息失败以后给broker的反馈。因此跟消费者启动、销毁以及消费有关的大部分方法都是在MQPushConsumer接口中。

1、启动消费者

还是先从消费者启动开始看,消费者启动的方法在DefaultMQPushConsumer的start()方法。

启动类中,先是检查一些参数的合法性,例如消费者名之类的。然后就是根据topic以及其相对应的表达式,构建当前消费者订阅的topic的信息,然后就是集群模式下将当前进程id设置为消费者实例的名称,再就是通过MQClient管理器创建一个MQClient用于跟broker交互。其实发现这些流程跟生产者的启动流程没有什么区别,就是多个一个构建订阅的topic的相关信息。 咱们接下来就是根据启动类中的步骤一个个分析。

 2、参数检查

检查一些常规参数是否为空,消费者组、消费模式等等,这个就不细说了。

 3、构建订阅的topic的信息

根据tags的过滤表达式来构建消费者订阅的不同topic的相关信息。

4、 集群模式下将当前进程id设置为消费者实例的名称

5、MQClient管理器创建一个MQClient

6、设置负载均衡策略

消费者的负载均衡策略很多,但是最常用还是以下两个。例如一共有三个broker,其中前两个都是有3个消息队列,最后一个有2个消息队列,这样就有8个消息队列,而消费者组中若有3个消费者。那么基于同一个消费者组下的消费者不可消费同一个topic下的同一个消息,那么就会有以下两种复杂均衡策略。

           

注:因为同一个消费者组下的消费者不能消费同一个topic下的同一个消息,因此,如果消费者的数量大于消息队列的个数,那么就有消费者没有消息可消费。例如上面有8个队列,但是有10个消费者,那么就注定有消费者没有消息可消费了。

7、读取上次消费消息的偏移量

若是广播模式,则消息的偏移量存储在本地,如果是集群模式,则消息的偏移量存储在远程。

若是广播模式,则根据解析出来的路径,读取本地存储偏移量的json,然后将messagequeue对应偏移量存储在ConcurrentMap

表中。

 

若是集群模式,本地并没有做额外的操作,broker收到请求以后会查询broker端存储的偏移量,然后做处理的,后面再说broker端的。

8、启动消费消息的服务

若是顺序消费,则初始化一个顺序消费消费的服务,然后启动,如果是并发消费消息,则初始化一个并发消费消息的服务,然后再启动。实现ConsumeMessageService接口的有两个,一个是顺序消息的,一个是并发消息的服务。

 

9、将当前消费者注册到MQClient实例中

10、启动

启动的时候加了同步锁,启动的程序中启动的东西也不少,咱们一个个将,获取namesrv的地址咱们就不说了。 

11、this.mQClientAPIImpl.start(),启动一个nettyClient跟broker交互。

12、this.startScheduledTask(),启动各种定时任务,定时获取namesrv地址,定时更新topic路由信息,定时发送心跳,定时持久化消费的偏移量。

13、先看这个,this.rebalanceService.start(),RebalanceService继承了Thread,所以看其启动的方法,主要是做了重新分发,具体怎么做的就不讲了,可以自己看看。

14、这个是重点,消息的拉取,this.pullMessageService.start(),PullMessageService继承了Thread,所以看其启动的方法,从拉取请求队列中获取一个拉取请求。

从拉去请求中获得消费者组然后获得消费者,然后impl.pullMessage(pullRequest);开始拉取消息 

 拉取方法中首先获取的就是ProcessQueue,我解释解释这个类,这个类可以理解为消息的中转站,拉取的消息都扔在这个队列中,然后被消费。如果这个队列被抛弃了,那就不用继续走下去了。

 

消息做流控,消息数量大于1000,或者消息大小大于100M则不再拉取 

拿到订阅主题的相关信息

 接下来就是拉取消息的回调,消息还没拉取到,先不看,等拉取以后咱们再回来看

构建系统拉取标识,标识哪个节点进行的拉取 

然后是消息的拉取,将回调函数都标明

根据brokerName获取broker的地址,然后根据topic更新topic的路由信息 

然后就是封装请求头,然后发起请求,到最后还是一个netty客户端发起请求。

15、Broker组装消息并反馈

Broker对于拉取的消息做出反馈的类是org.apache.rocketmq.broker.processor包下的PullMessageProcessor类的processRequest()方法。

 该方法中,前面大部分都是校验参数,咱们就不细说了

构建消息过滤的实体类,为后期过滤消息做准备 

 从MessageStore获取订阅的消息,然后构建反馈头准备反馈,接下来就是设置各种响应码。

 如果从从节点读取太慢,建议下次从主节点读取。

16、下面就是消费者收到broker的反馈了,我们直接看上面说的那个回调函数,PullCallback

消息被分装成pullResult

看到了吧,在这一步,消息被放到了processQueue中,然后processQueue又被给提交到消费者的消费线程池中。 

 

三、消息的推送

其实RocketMQ并没有实现推送,消息的推送其实还是基于消息的拉取。RocketMQ有个长轮询机制,每隔5s查询一次,看是否有自己需要的消息。但是这种效率不高,因此就有了另一个机制,在消息达到broker时进行存储的时候,监听器监控到新消息以后唤醒pull请求。

该方法在org.apache.rocketmq.store下的DefaultMessageStore.start()方法中。

在doReput()方法中有以下代码,如果不是从节点,意味着是主节点,代表读取到了数据,那么就会触发消息到达的监听器。 

然后拉取消息的服务就会被唤醒,然后处理消息,有兴趣的可以自己看 

四、并发消息和顺序消息

前面讲了consumer发送拉取消息的请求给broker,然后发送请求的时候还有一个消息的回调PullCallback,在PullCallback中把消息从封装的pullResult中拿出来放到了processQueue,然后再把processQueue交给consumeMessageService处理,其实就是在这里对消息进行了处理。

consumeMessageService是一个接口,它的实现类有两个ConsumeMessageConcurrentlyService和ConsumeMessageOrderlyService,分别是对顺序消息和并发消息的处理,我们一个个来讲。

1、并发消息(ConsumeMessageConcurrentlyService的submitConsumeRequest方法)

将消息传递过去,如果消息的条数小于32条,则将消息直接扔给消费者线程池。如果消息大于32条,那么就会分页,每页32条,然后一次交给线程池一页。

 

然后就看线程ConsumeRequest的run()方法,这里是ConsumeMessageConcurrentlyService中的内部类ConsumeRequest。如果processQueue被抛弃,那就不用走下去了,消息都在里面。再接着就是将消息交给消费的监听器。

如果之前设置了钩子函数,那么接下来就执行钩子函数的方法 

 

接下来就是将消息交给listen来继续处理了。

 

2、顺序消息(ConsumeMessageOrderlyService的submitConsumeRequest方法)

顺序消费一般都是局部顺序消息,都是对某个队列进行消息的顺序消费,因此不用担心消息的条数过多。

 然后就看线程ConsumeRequest的run()方法,这里是ConsumeMessageOrderlyService中的内部类ConsumeRequest。如果processQueue被抛弃,那就不用走下去了,消息都在里面。

接下来就是顺序消息和并发消息的区别,顺序消息这里使用了锁,对消息的处理流程加锁,只能一个个消费,至于对消息的处理还是跟并发消息的流程一样,主要是加了锁。

 

发布了69 篇原创文章 · 获赞 35 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/xiaoye319/article/details/102953592