与DefaultMQProducer类似,DefaultMQPushConsumer包含了defaultMQPushConsumerImpl,而defaultMQPushConsumerImpl又包含了MQClientInstance。
使用方式
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("s1");
consumer.setNamesrvAddr("{外网IP}:9876");
consumer.subscribe("mytopic", "mytag");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for(int i=0; i<msgs.length(); i++){
MessageExt msg = msgs.get(i);
System.out.println(msg.getTopic() + " " + msg.getTags() + " " + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
我们重点关注下subscribe和start的过程。
subscribe
- 订阅的时候会构造SubscriptionData,存储在rebalanceImpl.getSubscriptionInner()中
- SubscriptionData比较简单,包含topic,tag等信息,但是不包含主题的队列信息
start
观察MQClientInstance的start方法,重点关注以下三句
- this.startScheduledTask();
此处是开启一些定时任务,我们观察第二个定时任务
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
MQClientInstance.this.updateTopicRouteInfoFromNameServer();
} catch (Exception e) {
log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
}
}
}, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);
在10ms后更新主题路由信息,这里涉及到生产者生产的主题和订阅者关注的主题,对于这些主题,都会去nameServer获取路由信息(所在队列、broker等信息)。
关于订阅主题的路由信息,最终会存在this.rebalanceImpl.topicSubscribeInfoTable中。
- this.pullMessageService.start();
此处是对于订阅的队列,进行拉消息 - this.rebalanceService.start();
此处是做负载均衡,将订阅主题的队列分配到各个消费者
由于需等队列分配好后,才能进行拉消息,所以我们先看负载均衡的rebalanceService
rebalanceService
这里会遍历每一个consumer的每一个topic,对于每一个topic,会取出路由信息进行队列的分配,分配的时候会根据messageModel有不同的分配方式。
- broadcast 广播模式
广播模式,即消费组中的每个消费者消费每一个消息,所以需要监听每一个队列,无需分配。
每一个监听请求都封装成PullRequest,放到pullRequestQueue中。 - clustering 集群模式
集群模式,即消费组中的每个消费者平均分摊消息。
具体的分摊算法:
(1)将队列和消费者都进行排序
(2)使用配置的策略进行分摊,默认为AllocateMessageQueueAveragely
平均消费数=队列数/消费者数(如果除不尽有特殊处理)
按照消费者排序,每个消费者获取连续的一段队列,由于顺序和算法一致,所有消费者计算出来的结果是一致的
(3)根据分摊结果,更新pullRequestQueue
这里遇过一个问题,两个消费者以集群模式订阅主题(该主题拥有16个队列),但最终只有8个队列被消费。后来发现由于这两个消费者都在同一个机器不同的服务中,根据IP生成了一样的clientId(xxx:xxx:xxx:xxx@RocketMQTemplate)。分摊算法认为存在两个消费者,计算出平均消费数为16/2=8,然后为消费者排序,由于两个消费者clientId一致,被当成同一个消费者,分配了同样的8个队列,导致另外8个队列无法被消费。解决方案:消费者的clientId不能一致,重写clientId生成方法保证唯一性。
pullMessageService
pullMessageService负责从pullRequestQueue中取出消息,然后进行pull操作。
重点关注defaultMQPushConsumerImpl的pullMessage方法:
(1)构造PullCallback
(2)调用核心的拉取方法this.pullAPIWrapper.pullKernelImpl
这里面又是在调用MQClientAPIImpl的方法来通过NettyRemotingClient来与broker进行通讯,并在成功后调用callback方法。
(3)当拉取结果回来后,调用callback方法。callback方法中如果发现有消息,则会把消息放进processQueue中,并且提交消费请求(分为并发消费和有序消费,就是我们一开始注册的MessageListener),最后检查pullInterval来决定是立马继续拉取还是稍后拉取(默认interval为0)
总结
根据以上的源码分析,对消费者有了更深入的了解,我们可以在这几个角度对消费者进行分类
pull和push
- pull 是自主根据需求拉消息
- push 是自动推送消息,实际上也是客户端在拉消息,但是是不停的主动拉,拉取的时间间隔通过pullInterval来设置
广播和集群
广播是每个消费者消费所有队列,而集群需要根据一致的算法来分配队列,来达到共同消费的目的。分配队列的动作是在每个客户端中进行。
并发消费和有序消费。
无论是并发还是有序,都是多线程消费,但是对于有序消费,会在消费的时候对messageQueueLock进行上锁,来保证有序。