从零开始读RocketMq源码(四)Consumer启动流程解析

目录

前言

准备

订阅类型

启动消费者

源码解析

订阅消息

消费者初始化

订阅消费重试Topic

初始化rebalanceImpl

初始化pullAPIWrapper

初始化偏移量offsetStore

确定消费模式

启动消费模式服务

开启清除超时消息定时任务

高级特性-消息重试

判断消息类型

判断过期时间

选择重试策略

注册消费者实例

启动相关服务组件

总结


前言

前面我们讲到了生产者的启动以及发送消息到Broker中,然后再到Broker存储消息都进行了源码分析。有了Broekr这个中间“媒人”,那么光有生产者怎么能行呢,我们的消费者是不是也该出场了呢。接下来我们就对消费者的启动进行流程源码走读吧!

准备

源码地址:GitHub - rocketmq/rocketmq: Mirror of Apache RocketMQ

目前最新版本为:5.2.0

那么我们在idea上切换分支为 release-5.2.0

订阅类型

Consumer有两种从Broker中获取message的模式

  • 拉(pull)模式: 消费者主动从 Broker 拉取消息,适用于需要控制消息拉取频率和批量处理的场景
  • 推(push)模式: 消息由 Broker 主动推送给消费者,消费者在接收到消息后进行处理。推模式的特点是实时性较好,适用于对延迟敏感的场景

Conmuser有两种从Broker中消费Message的模式

  • 广播模式:每条消息会被消费者组中的买一个消费者实例消费一次
  • 集群模式每条消息只会被消费者组中的一个消费者实例消费

启动消费者

通过上面我们知道消费者有两种获取消息的方式,所以源码中官方提供了两个启动类:

  • PushConsumer
  • PullConsumer

本篇选择的是比较常用的PushConsumer推模式启动,源码实例位置和生产者在同一个包下

//源码位置
//包路径:org.apache.rocketmq.example.simple
//文件名:PushConsumer
//行数:32
public static void main(String[] args) throws InterruptedException, MQClientException {
    //实例话消费者对象并设置消费者组名
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
    //设置NameServer地址
    consumer.setNamesrvAddr(NAMESRV_ADDR);
    //设置订阅的Topic
    consumer.subscribe(TOPIC, "*");
    //设置消息偏移量类型
    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
    //注册消息监听器
    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
            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");
}
  • CONSUMER_GROUP消费者组必须先在控制台创建完成再设置,如果消费者组不存在启动会报错

控制台创建消费者参数说明

  1. clusterName:MQ集群名称
  2. brokerName:Broker名称
  3. groupName:消费者组名称
  4. consumeEnable:消费者开关
  5. consumeBroadcastEnable:集群模式开关,不开启则为广播模式
  6. retryQueueNums:消费失败重试次数
  7. brokerId
  8. whichBrokerWhenConsumeSlowly: 当消费者组中的某些消费者实例消费速度较慢,无法及时处理消息时,RocketMQ 可以通过配置该参数来指定从哪个 Broker 拉取消息,从而避免因消费速度不一致导致的消息堆积问题

  • TOPIC:topic后面设置的“*”表示获取全部tag标签不过滤,该参数是一个表达式,可以设置指定的tag标签(后面源码解析中会单独解析)

查看控制台可知,Broker中已经堆积了一定数量的消息,本篇我们订阅的是xxf-group消费者组

启动消费者实例,如果顺利打印日志,则表示消费成功了

打开控制台,查询消费者实例消息以及消息数量情况

源码解析

订阅消息

上面消费者启动的时候说到了topic以及相关tag标签的设置,是否好奇具体是咋个解析的呢?

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:1251
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subExpression);
this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);

首先创建了一个SubscriptionData对象,用于存储消费者的订阅信息

SubscriptionData对象有四个关键参数

  • topic:就是指定订阅的topic名称
  • subString:启动时设置的tag表达式
  • Set<String> tagsSet:如果subString不为"*",则会解析表达式后设置tag,与下面code一一对应
  • Set<Integer> codeSet:值为tagsSet中tag的hashCode,和上面一一对应

最后把封装好的SubscriptionData保存到一个map中ConcurrentMap<String /* topic */, SubscriptionData> subscriptionInnerkey为topic名称,value则为该对象

消费者初始化

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumer
//行数:761
setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
this.defaultMQPushConsumerImpl.start();

进入启动源码,你会发现基本和第一篇讲到的生产者启动流程一样

  • 首先对消费者组的名称进行非空判断以及名称长度校验等
  • 最后进入消费者启动核心方法

订阅消费重试Topic

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:1211
switch (this.defaultMQPushConsumer.getMessageModel()) {
    case BROADCASTING:
        break;
    case CLUSTERING:
        final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup());
        SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(retryTopic, SubscriptionData.SUB_ALL);
        this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData);
        break;
    default:
        break;
}

消息重试是RocketMq的高级特性。根据源码可知,消费重试功能只支持集群模式,前面我们控制台创建消费者时就设置了重试次数retryQueueNums

  • 消费重试会单独订阅一个retryTopic,专门用于消费重试,名称组成为%RETRY% + consumeGroupName
  • tag标签设置为“*”表示重试全部的消息
  • 最终将重试的订阅对象SubscriptionData也同样设置到subscriptionInner map中

下面会讲到消息重试的逻辑

初始化rebalanceImpl

rebalanceImpl该对象主要用于MessageQueue资源的重平衡,rebalance负载均衡可以这样理解

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:929
//设置消费者组名称
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
//设置消费模式
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
//设置分配消息队列策略
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);

如果消费者数量或者MessageQueue的数量发生变更时就会触发资源重平衡保证资源的合理分配

负载均衡策略通过AllocateMessageQueueStrategy设置

  • 轮询策略(AllocateMessageQueueAveragely):平均分配消息队列给消费者实例。
  • 一致性哈希策略(AllocateMessageQueueConsistentHash):基于一致性哈希算法分配消息队列,适用于消息处理需要顺序消费的场景。

当前只是初始化重平衡对象,后续逻辑中才会有相关操作,本篇不再深入解析

初始化pullAPIWrapper

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:934
if (this.pullAPIWrapper == null) {
    this.pullAPIWrapper = new PullAPIWrapper(
        mQClientFactory,
        this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
}
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
  • PullAPIWrapper 是一个封装类,用于处理消息拉取相关的逻辑
  • 最后就是注册消息过滤钩子,用于在消息拉取后对消息进行过滤处理

这里也只是初始化,具体拉取逻辑逻辑本篇不再深入

初始化偏移量offsetStore

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:945
case BROADCASTING:
    this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
    this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());

由源码我们知道,根据消费模式分为两种实例化方案

  • 广播模式:构建本地文件偏移量对象LocalFileOffsetStore, 消费进度会存储在本地文件系统中因为每个消费者实例都会消费所有的消息队列,所以需要每个消费者实例自己单独保存消费进度互不干扰
  • 集群模式:构建远程Broker偏移量对象RemoteBrokerOffsetStore, 消费进度会存储在Broker端 。因为 每个消息队列只会被消费者组中的一个消费者实例消费 , 消费进度在 Broker 端统一存储,消费者实例共享消费进度

确定消费模式

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:958
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
    this.consumeOrderly = true;
    this.consumeMessageService =
    new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
    //POPTODO reuse Executor ?
    this.consumeMessagePopService = new ConsumeMessagePopOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
    this.consumeOrderly = false;
    this.consumeMessageService =
    new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
    //POPTODO reuse Executor ?
    this.consumeMessagePopService =
    new ConsumeMessagePopConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}

根据上面源码我们可得,消费模式也有两种

  • 顺序消费-MessageListenerOrderly:按照消息发送的顺序进行消费
  • 并发消费-MessageListenerConcurrently:可以同时对多条数据进行消费,不考虑顺序

是否记得我们在前面启动消费者案例PushConsumer注册消息监听器时,已经重写了方法MessageListenerConcurrentlyconsumeMessage 函数,选择的是并发消费模式

是否看到还实例化了一个POPService对象,POP是什么?

POP 服务(POP Service)指的是一种优化的消息消费模式,通常用来提升消息消费的性能和效率。这里作个简单了解就行

启动消费模式服务

开启清除超时消息定时任务

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:973
this.consumeMessageService.start();

深入启动源码中,会返现里面维护了一个定时任务,清除过期的消息

第一次启动停顿时间:15m

每次间隔时间:15m

深入定时任务中的清除逻辑实现

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:ConsumeMessageConcurrentlyService
//行数:973
Iterator<Map.Entry<MessageQueue, ProcessQueue>> it =
this.defaultMQPushConsumerImpl.getRebalanceImpl().getProcessQueueTable().entrySet().iterator();

首先可以看到我们前面提到的重平衡出现了getRebalanceImpl()这里只是获取了内部维护的一个Map数据。然后循环map Values值进行处理

ConcurrentMap<MessageQueue, ProcessQueue> processQueueTable;

  • MessageQueue:为消息队列
  • ProcessQueue:消费者处理队列,相当于是MessageQueue对象的快照(本篇先简单了解,后面会单独一篇解析消费消息过程

processQueueTable是一个并发映射,用于存储消息队列(MessageQueue)与处理队列(ProcessQueue)的对应关系 MessageQueue与ProcessQueue是一一对应的

高级特性-消息重试

判断消息类型

进入ProcessQueue对象方法中

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:ProcessQueue
//行数:75
if (pushConsumer.isConsumeOrderly()) {
    return;
}
  • 顺序消息不支持消息重试这里进行了校验
判断过期时间
//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:ProcessQueue
//行数:87
//获取消息开始时间
String consumeStartTimeStamp = MessageAccessor.getConsumeStartTimeStamp(msgTreeMap.firstEntry().getValue());
//判断是否超时
if (StringUtils.isNotEmpty(consumeStartTimeStamp) && System.currentTimeMillis() - Long.parseLong(consumeStartTimeStamp) > pushConsumer.getConsumeTimeout() * 60 * 1000) {
    msg = msgTreeMap.firstEntry().getValue();
}

这里主要是在操作一个Map,TreeMap<Long, MessageExt> msgTreeMap他的作用是用于存储还没有消费的消息。

这里为什么使用的是TreeMap呢,而不是之前使用的CurrentMap?

  • TreeMap是一个有序队列,保证了键的自然顺序
  • TreeMap 中的取出第一条消息是一个高效的操作,因为 firstEntry() 是一个 O(1) 操作,直接返回第一个元素。
  • 不需要遍历整个队列,只需检查第一个元素,减少了时间复杂度和性能开销
  • 这里调用msgTreeMap.firstEntry().getValue(),表示取出第一条消息,因此第一条消息也是最早到达消费者的消息,后面消息以此类推。
  • 然后再判断前消息中的开始时间与当前时间做差,如果时间差超过默认的超时时间(15m,这说明消息已经超时了,需要进行后续的处理。
选择重试策略
//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:755
if (brokerName != null && brokerName.startsWith(MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX)
    || mq != null && mq.getBrokerName().startsWith(MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX)) {
    needRetry = false;
    sendMessageBackAsNormalMessage(msg);
} else {
    String brokerAddr = (null != brokerName) ? this.mQClientFactory.findBrokerAddressInPublish(brokerName)
    : RemotingHelper.parseSocketAddressAddr(msg.getStoreHost());
    this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, brokerName, msg,
                                                                      this.defaultMQPushConsumer.getConsumerGroup(), delayLevel, 5000, getMaxReconsumeTimes());
}

提供了两种重试策略,根据Broker名称的前缀来进行判断的

  1. 第一种如果Broker名称前缀加了 "syslo",不会进行重试,而是用生产者的方式重新发送一条新的消息给Broker深入sendMessageBackAsNormalMessage()方法会看到调用了this.mQClientFactory.getDefaultMQProducer().send(newMsg)
  2. 第二种则是正常的重试方式,通过消费者远程调用broker,并设置相关的参数包括如消费者组延迟级别(3)超时时间(5s)最大重试次数(16),最后会设置到一个请求头ConsumerSendMsgBackRequestHeader中进行发送。

默认情况下是使用第二种方式第一种需要特殊场景中,还需要设置一些特殊配置,正常情况下不会使用。

最后重试完成后,会将当前的超时消息从msgTreeMap中移除,然后开始下一轮处理,继续调用firstEntry() ,直到存在不满足条件的消息则直接结束本次定时任务,因为前面也说到过使用TreeMap,最前面的就是最先进来的消息那么如果第一个消息都没有超时,说明后面的所有消息更不可能超时了,就没有必要所有消息都遍历一次,这样大大提高了mq的性能。

注册消费者实例

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:977
boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);

这里的注册其实将消费者实例对象存储到一个消费者Map中ConcurrentMap<String, MQConsumerInner> consumerTable;key是消费者组名称,vlaue就是实例对象。这个在之前生产者启动时也提到过,生产者也有一个同样的Map用于存储生产者的实例producerTable

启动相关服务组件

//源码位置
//包路径:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:986
mQClientFactory.start();

这里的服务启动以及启动定时器和生产者一模一样,本篇就不在重复概述了


总结

本篇深入源码对消费者的启动流程有了一个更深层的理解,里面还讲到了对于TreeMap的使用,在MQ中大大提高了程序的性能。那么消费者启动成功了,就需要获取消息进行消费,这个操作又是怎么样实现的呢?下一篇我们继续深入源码对消费者消息的获取逻辑进行分析。

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RemainderTime

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值