RocketMQ源码阅读之消费者
RocketMQ消费者代码相对生产者来说要更复杂一些。本次阅读的消费者代码是使用push消费方式的源码实现,其设计底层其实只是将pull的方式进行封装从而实现push消费的效果。
创建消费者并启动
public static void main(String[] args) throws InterruptedException, MQClientException {
//下面使用的push的消费方式,pull的使用 new DefaultLitePullConsumer(),拉的方式需要自己保存偏移量和队列的关系
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("hr_consumer_test");
consumer.setNamesrvAddr("192.168.31.67:9876;192.168.31.68:9876");
consumer.subscribe("OrderMsg", "OrderTag");
//注册消费的方式:这里是并发消费 ,还可以设置顺序消费 MessageListenerOrderly
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
//默认情况下,每次都只是一条数据
for (MessageExt messageExt : msgs) {
System.out.println(new String(messageExt.getBody()));
}
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
消费者的创建过程比较简单,就不过多赘述,整个消费者的从start方法开始,这个方法最后调用的方法是DefaultMQPushConsumerImpl这个类中的start方法,这是整个start方法的核心方法。
/**
1. @Desc TODO 消费者启动的方法
2. @Author HeRong
3. @Date 2020/4/26
*/
public synchronized void start() throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
this.serviceState = ServiceState.START_FAILED;
//校验配置
this.checkConfig();
//拷贝订阅信息到内部默认的消费者类,集群模式下建立了重试队列
this.copySubscription();
//集群模式下,修改默认的实例名称为pid
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
this.defaultMQPushConsumer.changeInstanceNameToPID();
}
//创建实例:跟生产者调用的同一个方法
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
this.pullAPIWrapper = new PullAPIWrapper(
mQClientFactory,
this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
this.offsetStore.load();
//设置并发消费还是顺序消费
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
//TODO 启动消费服务,定时任务清理过期的消息,默认15分钟执行一次
this.consumeMessageService.start();
//注册消费服务到本地缓存 consumerTable ,
boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
this.consumeMessageService.shutdown();
throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
}
//TODO 启动消费者客户端 实例
mQClientFactory.start();
log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
this.serviceState = ServiceState.RUNNING;
break;
case RUNNING:
case START_FAILED:
case SHUTDOWN_ALREADY:
throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
+ this.serviceState
+ FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
null);
default:
break;
}
this.updateTopicSubscribeInfoWhenSubscriptionChanged();
this.mQClientFactory.checkClientInBroker();
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
this.mQClientFactory.rebalanceImmediately();
}
整个方法代码实现如上面,需要重点关注的几个地方是:
第一:创建客户端实例代码,在生产者源码启动的时候也是这样一行代码,这也是为什么我说不管生产者还是消费者,都是用这个客户端实例类进行抽象封装的。这里面创建了很多个服务,包括netty客户端,详细的说明可看之前的文章:RocketMQ源码阅读之生产者
第二: 注册了消费者和启动实例方法。在生产者源码阅读中我提到那里是注册了生产者,所以消费者相关的两个服务是没有作用的,因为没有数据。这里注册了消费者,所以就有数据了,也就是会触发消费。具体查看mQClientFactory.start()方法,这个方法之前生产者源码的文章我介绍过了RocketMQ源码阅读之生产者。但是只说了生产者相关的两个服务,今天说消费者相关的两个服务线程
拉取消息的服务
首先:this.pullMessageService.start();启动了一条守护线程,调用实现的业务如下:
/**
* @Desc TODO 启动守护线程,向阻塞队列中获取向生产者拉取消息的请求对象,处理拉取的请求
* @Author HeRong
* @Date 2020/4/24
*/
@Override
public void run() {
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
PullRequest pullRequest = this.pullRequestQueue.take();
this.pullMessage(pullRequest);
} catch (InterruptedException ignored) {
} catch (Exception e) {
log.error("Pull Message Service Run Method exception", e);
}
}
log.info(this.getServiceName() + " service end");
}
这里 pullRequestQueue = new LinkedBlockingQueue(); 这个变量是一个阻塞队列,也就是说这条线程当没有数据的时候会阻塞在这里等待放入请求到队列中。线程拿到请求后执行请求,跟踪方法最终调用到DefaultMQPushConsumerImpl这个类中的 pullMessage(final PullRequest pullRequest) 这个方法,这个方法业务逻辑比较复杂,且代码很长,但是方法体中有两个地方需要注意:
第一:创建了一个回调对象,里面有成功的回调方法
再查看回调方法体中如下图,
上图中可以看出,根据返回的状态执行不同的逻辑,最后更新请求将请求对象放回到之前说的阻塞队列中,如下图pullRequestQueue变量就是那个阻塞队列:
public void executePullRequestImmediately(final PullRequest pullRequest) {
try {
this.pullRequestQueue.put(pullRequest);
} catch (InterruptedException e) {
log.error("executePullRequestImmediately pullRequestQueue.put", e);
}
}
通过以上方式从而构成了一个死循环,持续不断的从broker拉取消息下来消费。再看查询到有消息的那个分支:
这里将查询到的消息交给了线程池,最终回到到了我们自定义的方法进行消息的消费了。
以上是回调对象中的逻辑,那回调方法是什么时候触发的呢?继续查看源代码,
这里通过异步的发送,并且将回调对象传递过去。在之前的文章RocketMQ源码阅读之生产者中我提过,通过netty发送异步请求最终会回调onSuccess方法并执行传递过来的回调方法。
总结上面消费流程就是: 守护线程 监听请求的阻塞队列 ——> 拿到请求对象——>构建回调函数并通过netty异步请求broker ——> 返回消息后执行回调函数——> 执行消费方法,更新请求信息——>将请求放到阻塞队列中——>下一个消费循环。
可能你会发现这个实现真个循环,但是开始的守护阻塞队列是空的,是哪里触发了第一次请求的呢?这就是下面要说的消费者负载均衡服务
负载均衡服务 rebalanceService.start()
这个服务也是启动了一个守护线程,每间隔20秒执行一次,调用的方法是客户端实例类的以下这个方法:
/**
* @Desc TODO 负载均衡的执行方法
* @Date 2020/4/24
*/
public void doRebalance() {
System.out.println(System.currentTimeMillis()+"====doRebalance begin=== consumerTable size = "
for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
MQConsumerInner impl = entry.getValue();
if (impl != null) {
try {
impl.doRebalance();
} catch (Throwable e) {
log.error("doRebalance exception", e);
}
}
}
}
这个方法的目的是每20秒从nameserver更新broker订阅信息,保证负载的合理。跟踪这个执行过程,最后会调用到一行代码:
this.dispatchPullRequest(pullRequestList);
这个方法在RebalancePushImpl类中有实现,如下:
public void dispatchPullRequest(List<PullRequest> pullRequestList) {
System.out.println("dispatchPullRequest time" + System.currentTimeMillis() +" size="+pullRequestList.size());
for (PullRequest pullRequest : pullRequestList) {
this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest);
log.info("doRebalance, {}, add a new pull request {}", consumerGroup, pullRequest);
}
}
最后调用的这个方法就是上面提到的将请求放入阻塞队列中的方法,从而触发了第一从消费者拉取消息,之后就循环往复的按照上面介绍的流程进行消息的拉取了。
上面提到的dispatchPullRequest这个方法,在RebalancePulImpl是一个空实现的方法,想必你应该想到了为啥。因为pull的消费方式是由我们自己实现拉取的,所以也就没必要实现它了。
以上是我对RocketMQ消费者流程的源码解读,过程中还涉及到很多点,我也没有全面的进行阅读到,只是对自己感兴趣的点进行了梳理,如有疑问可以留言交流。