RocketMQ 消费者消息回发 + 拉取消息 解析——图解、源码级解析

587 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情

消费者消息回发

如果发送消息出现异常等现象,消息消费者就会把消息回发给Broker服务器,以便等到消费者恢复后重新进行消息消费。

之前在发送消息源码中分析过,DefaultMQProducerImpl类中,this.defaultMQProducer.getDefaultMQProducerImpl().start(false)这行代码启动了内部默认的生产者,用于消费者消息回发(SendMessageBack

在这里插入图片描述

public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            // 初始时先标记为FAILED
            this.serviceState = ServiceState.START_FAILED;

    // 代码省略......
            
    // 回发消息        
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    
    // 代码省略......
}

当消费者处出现异常时,就会执行到this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();这个逻辑,即消息回发,源码如下:

public void sendHeartbeatToAllBrokerWithLock() {
    if (this.lockHeartbeat.tryLock()) {
        try {
            this.sendHeartbeatToAllBroker();
            this.uploadFilterClassSource();
        } // 省略......
}

可以看到主要就是执行了this.sendHeartbeatToAllBroker()this.uploadFilterClassSource()这两个方法,进入到重发消息的sendHeartbeatToAllBroker方法中,该方法主要负责发送心跳到所有存活着的Broker上,生产者只向Master发送心跳,消费者向Master和Slave都发送心跳:

private void sendHeartbeatToAllBroker() {
		// 封装Client要发送的心跳数据
        final HeartbeatData heartbeatData = this.prepareHeartbeatData();
        final boolean producerEmpty = heartbeatData.getProducerDataSet().isEmpty();
        final boolean consumerEmpty = heartbeatData.getConsumerDataSet().isEmpty();
        if (producerEmpty && consumerEmpty) {
            log.warn("sending heartbeat, but no consumer and no producer. [{}]", this.clientId);
            return;
        }
        // 省略......
}

在这里插入图片描述

消费者拉取消息(Pull)示例

消费者使用Pull方式拉取消息的流程和Push消息的流程基本类似,包括创建消费者对象、设置组名、启动消费者消费。代码如下:

public class PullConsumer {
    // 存储队列offset
    private static final Map<MessageQueue, Long> OFFSET_TABLE = new HashMap<>();

    public static void main(String[] args) throws Exception{
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("group A");
        // 启动消费者
        consumer.start();

        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("Target Topic");
        for (MessageQueue mq : mqs) {
            System.out.println("Consume message from " + mq);
            // 拉取消息
            PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, OFFSET_TABLE.get(mq), 32);
            System.out.println("pullResult : " + pullResult);
            // 设置该MQ的offset
            OFFSET_TABLE.put(mq, pullResult.getNextBeginOffset());
        }
        consumer.shutdown();
    }
}

将上面的流程概括一下:

  1. 初始化存储队列offsetMap,创建Pull模式的消费者对象
  2. 启动消费者消费
  3. 调用fetchSubscribeMessageQueues方法,根据Topic名称查询对应的MQ,主动拉取消息
  4. 循环遍历MQ,对于遍历到的每个MQ,取出一条消息

fetchSubscribeMessageQueues

获取所有MQ的方法源码如下,该方法位于org/apache/rocketmq/client/impl/MQAdminImpl.java中,执行步骤为:

  1. 从注册中心获取路由信息
  2. 如果路由信息不为空则获取路由信息中的队列集合
public Set<MessageQueue> fetchSubscribeMessageQueues(String topic) throws MQClientException {
        try {
        	// 从注册中心获取路由信息
            TopicRouteData topicRouteData = this.mQClientFactory.getMQClientAPIImpl().getTopicRouteInfoFromNameServer(topic, timeoutMillis);
            // 如果路由信息不为空则获取路由信息中的队列集合
            if (topicRouteData != null) {
                Set<MessageQueue> mqList = MQClientInstance.topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
                if (!mqList.isEmpty()) {
                    return mqList;
                } else {
                    throw new MQClientException("Can not find Message Queue for this topic, " + topic + " Namesrv return empty", null);
                }
            }
        } catch (Exception e) {
               // 省略......
        }
       // 省略......
    }

上述代码首先从注册中心中获取TopicRouteData,其中存储了路由信息:

在这里插入图片描述

  • orderTopicConf:顺序消息配置

它的格式为:BrokerName1:QueueId1;BrokerName2:QueueId2;......BrokerNameN:QueueIdN;

  • queueDatas:队列数据数组
  • brokerAddr:Broker数据数组
  • filterServerTable:Broker地址和Filter Server之间的映射

如果拿到的TopicRouteData不为空,则提取TopicRouteData内的QueueData生成MQ,这个MQ就是当前订阅的Topic下的。如果队列集合不为空,就会直接返回。


拉取消息的核心代码

拉取消息的核心方法是pullSyncImpl,在这个方法里实现了消息的拉取

private PullResult pullSyncImpl(MessageQueue mq, SubscriptionData subscriptionData, long offset, int maxNums, boolean block,
        long timeout)
        throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        this.isRunning();

        // 省略......
        
        this.subscriptionAutomatically(mq.getTopic());

        int sysFlag = PullSysFlag.buildSysFlag(false, block, true, false);

        long timeoutMillis = block ? this.defaultMQPullConsumer.getConsumerTimeoutMillisWhenSuspend() : timeout;

        boolean isTagType = ExpressionType.isTagType(subscriptionData.getExpressionType());
        // 拉取消息
        PullResult pullResult = this.pullAPIWrapper.pullKernelImpl(
            mq,
            subscriptionData.getSubString(),
            subscriptionData.getExpressionType(),
            isTagType ? 0L : subscriptionData.getSubVersion(),
            offset,
            maxNums,
            sysFlag,
            0,
            this.defaultMQPullConsumer.getBrokerSuspendMaxTimeMillis(),
            timeoutMillis,
            CommunicationMode.SYNC,
            null
        );
        
        // 对消息数据进行处理
        this.pullAPIWrapper.processPullResult(mq, pullResult, subscriptionData);
        // 如果namespace不是空的,则重置没有命名空间的Topic。
        this.resetTopic(pullResult.getMsgFoundList());
        
        // 把消息数据设置到上下文对象ConsumeMessageContext里
        if (!this.consumeMessageHookList.isEmpty()) {
            ConsumeMessageContext consumeMessageContext = null;
            consumeMessageContext = new ConsumeMessageContext();
            consumeMessageContext.setNamespace(defaultMQPullConsumer.getNamespace());
            consumeMessageContext.setConsumerGroup(this.groupName());
            consumeMessageContext.setMq(mq);
            consumeMessageContext.setMsgList(pullResult.getMsgFoundList());
            consumeMessageContext.setSuccess(false);
            this.executeHookBefore(consumeMessageContext);
            consumeMessageContext.setStatus(ConsumeConcurrentlyStatus.CONSUME_SUCCESS.toString());
            consumeMessageContext.setSuccess(true);
            this.executeHookAfter(consumeMessageContext);
        }
        return pullResult;
    }

其中,拉取消息采用的是this.pullAPIWrapper.pullKernelImpl方法,之后调用this.pullAPIWrapper.processPullResult方法对数据进行处理,如果namespace不是空的,则重置没有命名空间的Topic,在将消息数据设置到上下文对象ConsumeMessageContext里之后返回拉取消息的结果。