消息消费有两种模式:集群、广播
- 集群:topic下的同一条消息,只允许一个消费者消费
- 广播:topic下的同一条消息将被所有订阅该topic的消费者消费一次
此外,RocketMQ支持局部顺序消费,也就是保证一个消息队列上的消息顺序消费,不支持全局顺序消费,如果你的场景很特别,要求消息顺序消费,那么可以将该topic下的队列数设置为1,牺牲高可用;
消息消费分为推和拉两种模式,我们这里来看一下推模式的消息消费;
废话少说,我们直接从消息消费者启动来跟踪,先看DefaultMQPushConsumerImpl,直接看start方法,如下图:


关键步骤
- 检查配置
- 初始化消息进度
启动consumeMessageService,改类主要负责消息消费调用registerConsumer像MQClientInstance注册消费者,并启动MQClientInstance消息拉取
在MQClientInstance启动时,会启动PullMessageService,如下图:

从名字就可以看出这个类是专门负责RocketMQ客户端消息拉取的,该类是一个服务线程,类结构如下图:

run方法如下:

- 不断的从pullRequestQueue队列中取PullRequest,如果没取到,take方法会阻塞,直到有PullRequest可取
- 调用pullMessage方法处理PullRequest,跟踪进入该方法,可以看到根据消费组名从MQClientInstance中获取消费者内部实现类MQConsumerInner

消息拉取的基本流程,如下图:

1. 从pullRequest中获取 processQueue,并更新processQueue的lastPullTimestamp用来记录本次拉取的时间
2. 流控
- 如果当前处理消息条数超过了pullThresholdForQueue=1000,则放弃本次拉取任务,并且该队列的下一次拉取任务将在50ms后,才加入到拉去任务队列中
- processQueue中队列最大偏移量和最小偏移量的间距,不能超过consumeConcurrentlyMaxSpan=2000,否则也触发流控

3. 调用pullAPIWrapper.pullKernelImpl方法拉取消息,此处比较简单
- 获取broker地址
- this.mQClientFactory.getMQClientAPIImpl().pullMessage拉取消息
- 最终调用pullMessageAsync方法,使用Netty的api和消息服务器进行网络通信

消息拉取过程比较复杂,大家可以对照下图进行源码跟踪:

消息拉取长轮训机制(重点)
从上面的代码可以看出,RocketMQ并没有真正的实现服务器消息推送,而是由消费者主动去消息服务器拉取消息,RocketMQ的推模式是循环向消息服务器发送消息拉取请求,在消息未到达的时候,客户端并不是立即返回,而是采用了一种长轮训的方式,采用该模式的客户端,在没有消息到达的时候,会将线程在消息服务器端挂起,此时,消息服务器会每隔5s检查一次消息是否到达,如果有消息到达,则通知挂起的线程;
来看一段,PullMessageProcessor类(改类在broker中)中的processRequest代码

可以看到,在拉取消息失败的时候,根据是否开启了“允许挂起”,调用suspendPullRequest将请求挂起(其实是保存在Map中)

继续跟踪PullRequestHoldService的run方法

继续跟踪checkHoldRequest,根据偏移量判断是否有消息到来

到这里我们想一下,如果5秒检查一次是否有消息到达,那是不是太久了?有新消息来甚至还得等五秒客户端才知道;RocketMQ为了避免这种情况,引入了另一种机制:消息到达时,会调用notifyMessageArriving方法唤醒挂起线程;
ReputMessageService是处理消息的一个线程,用来将CommitLog中的消息转发到ConsumeQueue和IndexFile,同时,会将挂起(等待消息到来的客户端)的线程唤醒,如下图:

综上总结一下:
为什么RocketMQ不实现服务端主动推送消息?
RocketMQ的设计追求极简主义,追求简单高效,这和它的设计理念是密不可分的;
如何消息消费的高性能?
答:客户端主动长轮训拉取
如何保证消息消费的及时性?
答:服务端在新消息到达时主动通知挂起线程
















