producer源码结构如下:
我们通常使用mq接受消息,实例化consumer的方式就是:
DefaultMQPushConsumer consumer =
new
DefaultMQPushConsumer(
"MyTopic-Consume-Single"
);
//实际调用了
public
DefaultMQPushConsumer(String consumerGroup) {
this
(consumerGroup, (RPCHook)
null
,
new
AllocateMessageQueueAveragely());
//注意此处默认创建了一个消费负载均衡策略
}
|
所以我们就从DefaultMQPushConsumer开始说起吧。
DefaultMQPushConsumer继承了ClientConfig并且实现了MQPushConsumer接口。与producer类似,同时注入一个重要的属性
protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
public
class
DefaultMQPushConsumer
extends
ClientConfig
implements
MQPushConsumer {
protected
final
transient
DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
|
MQPushConsumer接口定义了一些最基本的方法,例如:
void
registerMessageListener(
final
MessageListenerConcurrently messageListener);
//消费者注册监听器。
void
subscribe(
final
String topic,
final
String subExpression)
throws
MQClientException;
//设置订阅的topic以及tag的方法。
|
ClientConfig在producer里已经介绍过了,就不重复说了。
而消费消息大致流程如下,我们具体看看各个步骤都做了什么。
consumer.setNamesrvAddr(
"10.3.254.52:9876"
);
//设置namesrv,实际是调用ClientConfig.setNamesrvAddr(String namesrvAddr);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//设置消费起始位置。
consumer.setConsumeMessageBatchMaxSize(
5
);
//设置每次消费消息数量。
consumer.subscribe(
"TopicTest"
,
"*"
);
//设置订阅的topic和tag。
consumer.registerMessageListener(
new
StockListener() );
//注册消费端的监听器,分为普通消费监听器和顺序消费监听器。其实就是把StockListener赋值给DefaultMQPushConsumer的MessageListener属性。
consumer.start();
//启动消费者。
|
重点看一下,启动消费者都干了些什么?
consumer.start();该方法实际调用了DefaultMQPushConsumerImpl .start() 【以下环节都是针对普通消费顺序】
1、首先进行状态检查,如果是CREATE_JUST才进行以下操作:
2、将状态置为START_FAILED。
3、this.checkConfig();进行参数检查,检查范围比较多,大致如下:
3.1)consumerGroup不能为空,不能含有非法字符,长度不能超过255。
3.2) 不能为默认的消费者组名"DEFAULT_CONSUMER"。
3.3)消费模型不能为空,即必须是集群消费或者广播消费的一种【默认是集群消费】
3.4)起始消费位置不能为空。【只对初次消费有效】
3.5)消费时间戳不能为空 -- (消息回溯,默认默认值是半小时以前)【只对初次消费有效】
3.6)消费负载均衡策略不能为空,默认是AllocateMessageQueueAveragely。下面我们看看,这个策略是怎么做到消费负载均衡的。
public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll, List<String> cidAll) ;这个方法就是做消费负载均衡的。他的参数currentCID代表这个消费者组的其中一个消费者 id,List<MessageQueue> mqAll代表所有的订阅的topic下所有的messageQueue, List<String> cidAll代表消费者组的消费者集合。该方法最终会为每个消费者分配对应的messageQueue。具体算法查看源码。例如,一个8个 messageQueue,消费者组内有三个消费者。
最终分配情况大致如下:
sonsumer0 consumer1 consumer2
queue0 queue3 queue6
queue1 queue4 queue7
queue2 queue5
3.7)存储订阅关系的subscription不能为空,private Map<String /* topic */, String /* sub expression */> subscription = new HashMap<String, String>();
3.8)消费端注册的监听器不能为空,并检查是普通消费还是顺序消费,并且必须是这二者其一。
3.9)检查消费者默认线程池最小和最大是否是在1~1000且最小值不能大于最大值。【默认最小20,最大64】每1分钟调整一次线程池,这也是针对消费者来说的,具体为如果消息堆积超过10W条,则调大线程池,最多64个线程;如果消 息堆积少于8W条,则调小线程池。
3.10)检查单队列并行消费最大跨度consumeConcurrentlyMaxSpan,不能小于1不能大于65535。consumeConcurrentlyMaxSpan这个值默认是2000,当RocketMQ发现本地缓存的消息的最大值-最小值差距大于这个值(2000)的时候,会 触发流控——也就是说如果头尾都卡住了部分消息,达到了这个阈值就不再拉取消息。
3.11)检查拉消息本地队列缓存消息最大数pullThresholdForQueue,不能小于1,大于65535。【默认是1000】。含义是:消费者不间断的从broker拉取消息,消息拉取到本地队列,然后本地消费线程消费本地消息队列,只是一个异步过 程,拉取线程不会等待本地消费线程,这种模式实时性非常高(本地消息队列达到解耦的效果,响应时间减少)。对消费者对本地队列有一个保护,因此本地消息队列不能无限大,否则可能会占用大量内存。ps:还记得broker启动至少需要4G的磁盘吗?还记得每条消息的最大值默认是4M吗?那这里设置的1000是巧合呢还是有意为之?
3.12)检查pullThresholdForTopic值是否为默认的-1,如果不是则必须在1~65535之间。【表示每个topic在本地缓存最多的消息条数】
3.13)检查消息缓存值pullThresholdSizeForQueue,不能小于1M,不能大于1024M。【默认是100M】
3.14)检查每次批量消费规模consumeMessageBatchMaxSize,不能小于1条,不能大于1024条。【默认是1条】
3.15)检查每次从broker批量拉取消息数量pullBatchSize,不能小于1条,不能大于1024条。【默认32条】
4、调用this.copySubscription();Client端信号收集,拷贝订阅信息,将消费者的topic订阅关系设置到rebalanceImpl的SubscriptionInner的map中用于负载。
5、如果是集群消费模式,则将客户端实例名由"DEFAULT"变成客户端实例的进程号。
6、调用MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);方法,以当前consumer作为参数实例化一个消费端实例。
7、接着完善rebalanceImpl实例,给他设置消费者组,消费模型,消费端负载均衡策略,以及消费端实例。
8、构建PullAPIWrapper对象,该对象封装了具体拉取消息的逻辑,PULL,PUSH模式最终都会调用PullAPIWrapper类的方法从Broker拉取消息。
9、this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);又是什么钩子,感觉可有可无,因为跟进去发现接口定义的方法没被实现。