RocketMQ源码(十八)—DefaultMQPushConsumer处理Broker的拉取消息响应源码

此前我们梳理了RocketMQ源码(十六)—DefaultMQPushConsumer消费者发起拉取消息请求源码_代码---小白的博客-CSDN博客_defaultmqpushconsumer

以及 RocketMQ源码(十七)—Broker处理DefaultMQPushConsumer发起的拉取消息请求源码_代码---小白的博客-CSDN博客

,现在我们来学习Consumer如何处理Broker的拉取消息响应的源码。

1 客户端异步请求回调 

此前我们梳理consumer发起拉取消息请求的时候,通过ASYNC模式异步的进行拉取,并且InvokeCallback#operationComplete方法将会在得到结果之后进行回调,内部调用pullCallback的回调方法。

在回调方法中,如果解析到了响应结果,那么调用pullCallback#onSuccess方法处理,否则调用 

pullCallback#onException方法处理。


    /**
     * MQClientAPIImpl的方法
     * 异步的拉取消息,并且触发回调函数
     * @param addr              broker地址
     * @param request           请求命令对象
     * @param timeoutMillis     消费者消息拉取超时时间,默认30s
     * @param pullCallback      拉取到消息之后调用的回调函数
     * @throws RemotingException
     * @throws InterruptedException
     */
    private void pullMessageAsync(
        final String addr,
        final RemotingCommand request,
        final long timeoutMillis,
        final PullCallback pullCallback
    ) throws RemotingException, InterruptedException {
        /**
         * 基于netty给broker发送异步消息,设置一个InvokeCallback回调对象
         * InvokeCallback#operationComplete方法将会再得到结果之后进行回调,内部调用pullCallback的回调方法
         */
        this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
            /**
             * 异步执行的回调方法
             * @param responseFuture
             */
            @Override
            public void operationComplete(ResponseFuture responseFuture) {
                //返回命令对象
                RemotingCommand response = responseFuture.getResponseCommand();
                if (response != null) {
                    try {
                        //解析响应获取结果
                        PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response, addr);
                        assert pullResult != null;
                        //如果解析到了结果,那么调用pullCallback#onSucess方法处理
                        pullCallback.onSuccess(pullResult);
                    } catch (Exception e) {
                        pullCallback.onException(e);
                    }
                } else {
                    //没有结果,就调用onException方法处理异常
                    if (!responseFuture.isSendRequestOK()) {
                        //发送失败
                        pullCallback.onException(new MQClientException(ClientErrorCode.CONNECT_BROKER_EXCEPTION, "send request failed to " + addr + ". Request: " + request, responseFuture.getCause()));
                    } else if (responseFuture.isTimeout()) {
                        //超时
                        pullCallback.onException(new MQClientException(ClientErrorCode.ACCESS_BROKER_TIMEOUT, "wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request,
                            responseFuture.getCause()));
                    } else {
                        pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause()));
                    }
                }
            }
        });
    }

1.1 processPullResponse解析响应

该方法处理response获取PullResult,根据响应的数据创建PullResultExt对象返回,注意此时拉取到的消息还是一个字节数组。

  /**
     * MQClientAPIImpl的方法
     * 该方法处理response获取PullResult,根据响应的数据创建PullResultExt对象返回,注意此时拉取到的消息还是一个字节数组
     * @param response
     * @param addr
     * @return
     * @throws MQBrokerException
     * @throws RemotingCommandException
     */
    private PullResult processPullResponse(
        final RemotingCommand response,
        final String addr) throws MQBrokerException, RemotingCommandException {
        PullStatus pullStatus = PullStatus.NO_NEW_MSG;
        //设置结果状态码
        switch (response.getCode()) {
            case ResponseCode.SUCCESS:
                pullStatus = PullStatus.FOUND;
                break;
            case ResponseCode.PULL_NOT_FOUND:
                pullStatus = PullStatus.NO_NEW_MSG;
                break;
            case ResponseCode.PULL_RETRY_IMMEDIATELY:
                pullStatus = PullStatus.NO_MATCHED_MSG;
                break;
            case ResponseCode.PULL_OFFSET_MOVED:
                pullStatus = PullStatus.OFFSET_ILLEGAL;
                break;

            default:
                throw new MQBrokerException(response.getCode(), response.getRemark(), addr);
        }

        //解析响应头
        PullMessageResponseHeader responseHeader =
            (PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class);

        //根据响应的数据创建PullResultExt对象返回,此时拉取到的消息还是一个字节数组
        return new PullResultExt(pullStatus, responseHeader.getNextBeginOffset(), responseHeader.getMinOffset(),
            responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody(), responseHeader.getOffsetDelta());
    }

2 PullCallback回调

在processPullResponse处理response之后,会调用此前DefaultMQPushConsumerImpl#pullMessage方法中创建的PullCallback消息拉取的回调函数,执行onSuccess回调方法。如果解析过程中抛出异常,则调用onException方法。

onSuccess回调方法的大致逻辑为:

1. 调用processPullResult方法处理pullResult,进行消息解码、过滤以及设置其他属性的操作,返回pullResult。

2. 如果没有拉取到消息,那么设置下一次拉取的起始offset到PullResult中,调用executePullResultImmediately方法立即将拉取请求再次放入PullMessageService的pullRequestQueue,PullMessageService是一个线程服务,PullMessageService将会循环的获取

pullRequestQueue中的pullRequest然后向broker发起新的拉取消息请求,进行下次消息的拉取。

3. 如果拉取到了消息,将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中,等待被异步的消费。

4. 通过consumeMessageService将拉取到的消息构建为ConsumeRequest,然后通过内部的consumeExecutor线程池消费消息,consumeMessageService有ConsumeMessageConcurrentlyService并发消费和ConsumeMessageOrderlyService顺序消费两种实现。
5. 获取配置的消息拉取间隔,默认为0,如果大于0则调用executePullRequestLater方法,等待间隔时间后将拉取请求再次放入pullRequestQueue中,否则立即调用executePullRequestImmediately放入pullRequestQueue中,进行下次消息的拉取。

如果是onException方法,那么延迟3s将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取。

        //TODO: 构建消息处理的回调对象,它的非常重要的,等从broker拉取消息后(这里是从broker拉取消息成功后才执行的),会交给它来处理
        PullCallback pullCallback = new PullCallback() {
            @Override
            public void onSuccess(PullResult pullResult) {
                if (pullResult != null) {
                    /**
                     * 拉取成功,开始处理从broker读取到的消息
                     * 将二进制内容转换成MessageExt对象,并根据TAG的值进行过滤
                     */
                    pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
                        subscriptionData);

                    switch (pullResult.getPullStatus()) {
                        //TODO: 发现了消息
                        case FOUND:
                            long prevRequestOffset = pullRequest.getNextOffset();
                            //TODO: 更新nextOffset的值
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                            long pullRT = System.currentTimeMillis() - beginTimestamp;
                            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
                                pullRequest.getMessageQueue().getTopic(), pullRT);

                            long firstMsgOffset = Long.MAX_VALUE;
                            /**
                             * TODO:如果没有消息则立即执行,立即拉取的意思是继续将PullRequest放入队列中
                             * 这样take()方法将不会再阻塞,然后继续从broker拉取消息,从而达到持续从broker拉取消息
                             */
                            if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
                                //拉取消息
                                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                            } else {
                                //拉取的消息不为空
                                firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();

                                DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
                                    pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());

                                //TODO: 将本次读取到的所有信息(经过TAG/SQL过滤)保存到本地缓存队列processQueue中
                                boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
                                /**
                                 * TODO: 构建consumeRequest,将消息提交到线程池中,由ConsumerMessageService 进行消费
                                 * 由于我们的是普通消息(不是顺序消息),所以由ConsumeMessageConcurrentlyService类来消费消息
                                 * ConsumeMessageConcurrentlyService内部会创建一个线程池ThreadPoolExecutor,这个xcc非常重要,消息最终将提交到这个线程池中
                                 */
                                DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                                    pullResult.getMsgFoundList(),
                                    processQueue,
                                    pullRequest.getMessageQueue(),
                                    dispatchToConsume);

                                /**
                                 * 上面是异步消费,然后这里是将PullRequest放入队列中,这样take()方法将不会阻塞
                                 * 然后继续从broker中拉取消息,从而到达持续从broker中拉取消息
                                 * 延迟pullInterval 时间再去拉取消息:
                                 *      这里有一个 pullInterval参数,表示间隔多长时间在放入队列中(实际上就是间隔多长时间再去broker拉取消息)。当消费者消费速度比生产者快的时候,可以考虑设置这个值,这样可以避免大概率拉取到空消息。
                                 *
                                 */
                                if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
                                    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
                                        DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
                                } else {
                                    //立即拉取消息
                                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                                }
                            }

                            if (pullResult.getNextBeginOffset() < prevRequestOffset
                                || firstMsgOffset < prevRequestOffset) {
                                log.warn(
                                    "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
                                    pullResult.getNextBeginOffset(),
                                    firstMsgOffset,
                                    prevRequestOffset);
                            }

                            break;
                        case NO_NEW_MSG:
                        case NO_MATCHED_MSG:
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());

                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);

                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                            break;
                        case OFFSET_ILLEGAL:
                            log.warn("the pull request offset illegal, {} {}",
                                pullRequest.toString(), pullResult.toString());
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());

                            pullRequest.getProcessQueue().setDropped(true);
                            DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {

                                @Override
                                public void run() {
                                    try {
                                        DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
                                            pullRequest.getNextOffset(), false);

                                        DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());

                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());

                                        log.warn("fix the pull request offset, {}", pullRequest);
                                    } catch (Throwable e) {
                                        log.error("executeTaskLater Exception", e);
                                    }
                                }
                            }, 10000);
                            break;
                        default:
                            break;
                    }
                }
            }

            /**
             * TODO: 如果拉取出现异常,则执行异常回调
             * 如果broker没有消息,则将拉取请求挂起,然后返回一个null对象给消费者,消费者如果拿到的是null,则是为异常情况,然后执行异常回调
             * 所以说,如果没有消息,则broker将拉取请求挂起,其目的是如果有消息到达,能立即写给消费者;同时消费者也会每隔3s去broker拉取一次,如果这次依然没有消息,则继续将本次请求挂起
             */
            @Override
            public void onException(Throwable e) {
                if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    log.warn("execute the pull request exception", e);
                }

                //TODO: 延迟3s钟,将PullRequest对象再次放入队列pullRequestQueue中,等待再次take(),然后继续拉取消息的逻辑
                DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
            }
        };

2.1 processPullResult处理拉取结果

处理pullResult,进行消息解码、过滤以及设置其他属性的操作。

1. 更新下次拉取建议的brokerId,下次拉取消息时从pullFromWhichNodeTable中直接取出。

2. 对消息二进制字节数组进行解码转换为java的List消息集合。

3. 如果存在tag,并且不是classFilterMode,那么按照tag过滤消息,这就是客户端的消息过滤。这采用String#equals方法过滤,而broker端则是比较的tagHash值,即hashCode。

4. 如果有消息过滤钩子,那么执行钩子方法,这里可以扩展自定义的消息过滤的逻辑。

5. 遍历过滤通过的消息,设置属性。例如事务id,最大、最小偏移量、brokerName。

6. 将过滤后的消息存入msgFoundList集合

7. 因为消息已经被解析了,那么设置消息的字节数组为null,释放内存。


    /**
     * PullAPIWrapper的方法
     * 拉取成功,开始处理消息
     * @param mq
     * @param pullResult
     * @param subscriptionData
     * @return
     */
    public PullResult processPullResult(final MessageQueue mq, final PullResult pullResult,
        final SubscriptionData subscriptionData) {
        PullResultExt pullResultExt = (PullResultExt) pullResult;

        //更新下次拉取建议的brokerId,下次拉取消息时从pullFromWhichNodeTable中直接取出
        this.updatePullFromWhichNode(mq, pullResultExt.getSuggestWhichBrokerId());
        //如果成功拉取到消息
        if (PullStatus.FOUND == pullResult.getPullStatus()) {
            //解析消息
            ByteBuffer byteBuffer = ByteBuffer.wrap(pullResultExt.getMessageBinary());
            //TODO: 将二进制内容转换成MessageExt对象
            List<MessageExt> msgList = MessageDecoder.decodesBatch(
                byteBuffer,
                this.mQClientFactory.getClientConfig().isDecodeReadBody(),
                this.mQClientFactory.getClientConfig().isDecodeDecompressBody(),
                true
            );
            // 根据订阅信息消息 tag 匹配合适消息,这里会过滤掉没有订阅的 tag 消息
            boolean needDecodeInnerMessage = false;
            for (MessageExt messageExt: msgList) {
                if (MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)
                    && MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.NEED_UNWRAP_FLAG)) {
                    needDecodeInnerMessage = true;
                    break;
                }
            }
            if (needDecodeInnerMessage) {
                List<MessageExt> innerMsgList = new ArrayList<>();
                try {
                    for (MessageExt messageExt: msgList) {
                        if (MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)
                            && MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.NEED_UNWRAP_FLAG)) {
                            MessageDecoder.decodeMessage(messageExt, innerMsgList);
                        } else {
                            innerMsgList.add(messageExt);
                        }
                    }
                    msgList = innerMsgList;
                } catch (Throwable t) {
                    log.error("Try to decode the inner batch failed for {}", pullResult.toString(), t);
                }
            }

            List<MessageExt> msgListFilterAgain = msgList;
            //TODO: 根据TAG的值进行过滤
            if (!subscriptionData.getTagsSet().isEmpty() && !subscriptionData.isClassFilterMode()) {
                msgListFilterAgain = new ArrayList<>(msgList.size());
                for (MessageExt msg : msgList) {
                    if (msg.getTags() != null) {
                        if (subscriptionData.getTagsSet().contains(msg.getTags())) {
                            msgListFilterAgain.add(msg);
                        }
                    }
                }
            }

            if (this.hasHook()) {
                FilterMessageContext filterMessageContext = new FilterMessageContext();
                filterMessageContext.setUnitMode(unitMode);
                filterMessageContext.setMsgList(msgListFilterAgain);
                this.executeHook(filterMessageContext);
            }

            //设置消息队列当前最小/最大位置到消息拓展字段
            for (MessageExt msg : msgListFilterAgain) {
                String traFlag = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
                if (Boolean.parseBoolean(traFlag)) {
                    msg.setTransactionId(msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX));
                }
                MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MIN_OFFSET,
                    Long.toString(pullResult.getMinOffset()));
                MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MAX_OFFSET,
                    Long.toString(pullResult.getMaxOffset()));
                msg.setBrokerName(mq.getBrokerName());
                msg.setQueueId(mq.getQueueId());
                if (pullResultExt.getOffsetDelta() != null) {
                    msg.setQueueOffset(pullResultExt.getOffsetDelta() + msg.getQueueOffset());
                }
            }

            //TODO: 将过滤后的消息给消费者消费覅
            pullResultExt.setMsgFoundList(msgListFilterAgain);
        }

        //清空消息二进制数组
        pullResultExt.setMessageBinary(null);

        return pullResult;
    }

2.2 executePullRequestImmediately再次拉取消息

将拉取请求再次放入PullMessageService的pullRequestQueue中,PullMessageService是一个线程服务。PullMessageService将会循环的获取pullRequestQueue中的pullRequest然后向broker发起新的拉取消息请求,进行下次消息的拉取。

 /**
     * DefaultMQPushConsumerImpl的方法
     * 下一次消息拉取
     * @param pullRequest   拉取请求
     */
    public void executePullRequestImmediately(final PullRequest pullRequest) {
        //调用PullMessageService#executePullRequestImmediately方法
        this.mQClientFactory.getPullMessageService().executePullRequestImmediately(pullRequest);
    }

  /**
     * DefaultMQPushConsumerImpl的方法
     * 下一次消息拉取
     * 这里就是重点了
     * 它将PullRequest对象放入了pullRequestQueue队列中,这个队列就是PullMessageService从pullRequestQueue队列中获取pullRequest对象的队列。
     * 刚开始获取不到,它会一直阻塞,现在经过重平衡后,队列中有了数据,现在就可以获取了,然后接下来就是拉取消息。
     * @param pullRequest
     */
    public void executePullRequestImmediately(final PullRequest pullRequest) {
        try {
            this.messageRequestQueue.put(pullRequest);
        } catch (InterruptedException e) {
            logger.error("executePullRequestImmediately pullRequestQueue.put", e);
        }
    }

2.3 putMessage存放消息

该方法将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中。

返回是否需要分发消费dispatchToConsume,当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,将会返回true。

dispatchToConsume对并发消费无影响,只对顺序消费有影响。

/**
 * ProcessQueue的方法
 * 消息存入msgTreeMap这个红黑树map集合中
 *
 * @param msgs 一批消息
 * @return 是否需要分发消费,当当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,将会返回true
 */
public boolean putMessage(final List<MessageExt> msgs) {
    boolean dispatchToConsume = false;
    try {
        //尝试加写锁防止并发
        this.treeMapLock.writeLock().lockInterruptibly();
        try {
            int validMsgCnt = 0;
            for (MessageExt msg : msgs) {
                //当该消息的偏移量以及该消息存入msgTreeMap
                MessageExt old = msgTreeMap.put(msg.getQueueOffset(), msg);
                if (null == old) {
                    //如果集合没有这个offset的消息,那么增加统计数据
                    validMsgCnt++;
                    this.queueOffsetMax = msg.getQueueOffset();
                    msgSize.addAndGet(msg.getBody().length);
                }
            }
            //消息计数
            msgCount.addAndGet(validMsgCnt);
            //当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,dispatchToConsume = true,consuming = true
            if (!msgTreeMap.isEmpty() && !this.consuming) {
                dispatchToConsume = true;
                this.consuming = true;
            }
            //计算broker累计消息数量
            if (!msgs.isEmpty()) {
                MessageExt messageExt = msgs.get(msgs.size() - 1);
                String property = messageExt.getProperty(MessageConst.PROPERTY_MAX_OFFSET);
                if (property != null) {
                    long accTotal = Long.parseLong(property) - messageExt.getQueueOffset();
                    if (accTotal > 0) {
                        this.msgAccCnt = accTotal;
                    }
                }
            }
        } finally {
            this.treeMapLock.writeLock().unlock();
        }
    } catch (InterruptedException e) {
        log.error("putMessage exception", e);
    }

    return dispatchToConsume;
}

2.4 消息的两次过滤

通过前面的源码可以看到,消息实际上经过了两次过滤,一次是在broekr中,一次是拉取到consumer之后,为什么经过两次过滤呢?因为broker中的过滤是比较的hashCode值,而hashCode存在哈希碰撞的可能,因此hashCode对比相等之后,还需要在consumer端进行equals的比较,再过滤一次。

为什么服务端不直接进行equals过滤呢?因为tag的长度是不固定的,而通过hash算法可以生成固定长度的hashCode值,这样才能保证每个consumequeue索引条目的长度一致。而tag的真正值保存在commitLog的消息体中,虽然broker最终会获取到commitLog中的消息并返回,但是获取的一段消息字节数组,并没有进行反序列化为Message对象,因此无法获取真实值,而在consumer端一定会做反序列化操作的,因此tag的equals比较放在了consumer端。

3 总结

本次我们来学习Consumer如何处理Broker的拉取消息响应的源码。入口就是MQClientAPIImpl#pullMessageAsync方法内部的回调函数InvokeCallback#operationComplete方法。

1. 在这个方法中,首先进行消息的解码以及第二次过滤,然后将消息存入对应的processQueue处理队列内部的msgTreeMap中

2. 然后通过consumeMessageService#submitConsumeRequest方法将拉取到的消息构建为ConsumeRequest,然后通过内部的consumeExecutor线程池的消费消息。

        2.1 consumeMessageService有ConsumeMessageConcurrentlyService并发消费和ConsumeMessageOrderlyService顺序消费两种实现。

3. 最后是再次发起消息拉取请求。

参考资料:

RocketMQ源码(20)—DefaultMQPushConsumer处理Broker的拉取消息响应源码_刘Java的博客-CSDN博客

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值