rocketmq 消息消费后处理机制

一句话概述:消息消费后有两个地方做ack。第一个地方是消费失败后同步将消费失败的消息发送回broker,另一个地方是定时任务(参见org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#start(boolean)

--> org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#persistAll)

消息拉取

以push模式消费为例,消息拉取与消费的入口核心代码:

org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage

PullCallback pullCallback = new PullCallback() {
            @Override
            public void onSuccess(PullResult pullResult) {
                if (pullResult != null) {
                    pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult, subscriptionData);

                    switch (pullResult.getPullStatus()) {
                        case FOUND:
                            long prevRequestOffset = pullRequest.getNextOffset();
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                            long pullRT = System.currentTimeMillis() - beginTimestamp;
                            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
                                pullRequest.getMessageQueue().getTopic(), pullRT);

                            long firstMsgOffset = Long.MAX_VALUE;
                            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());

//将消息放入ProcessQuesu中;校验该队列是否在消费中,只会对顺序消费的consumeMessageService有影响,如果正在消费则不提交任务
                                boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
                                
//如果拉取到消息,会将消息封装成一个ConsumeRequest对象放入队列中待消费
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                                    pullResult.getMsgFoundList(),
                                    processQueue,
                                    pullRequest.getMessageQueue(),
                                    dispatchToConsume);

                                if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
                                    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
                                } else {
                                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                                }
                            }

org.apache.rocketmq.client.impl.consumer.ProcessQueue#putMessage 方法是将拉取到的消息放入到客户端本地的processQueue中,processQueue是消费端对broker端消费队列逻辑上的一个镜像;客户端拉取消息后都会先将消息存入processqueue中再进行消费;

在broker端处理消息拉取的processor是 PullMessageProcessor,在成功拉取到消息后会根据是否需要保存消费进度将拉取的进度保存在内存中

if (storeOffsetEnable) {
            this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(channel),requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset());
        }

提交消费

代码回到提交client 提交 ConsumeRequest的代码,在client端会根据是并发消费还是顺序消费执行不同的逻辑;

DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                                    pullResult.getMsgFoundList(),
                                    processQueue,
                                    pullRequest.getMessageQueue(),
                                    dispatchToConsume);

并发消费 

执行方法 

org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService#submitConsumeRequest

这里会根据 consumeMessageBatchMaxSize 做分批次将消息分批分装进多个ConsumeRequest任务,该类是一个Runnale类,我们看看它的run方法:

@Override
        public void run() {
            if (this.processQueue.isDropped()) {
                log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue);
                return;
            }

//NO.1
            MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener;            

...省略代码...
                
//NO.2
ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
            }

            long beginTimestamp = System.currentTimeMillis();
            boolean hasException = false;
            ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
            try {
                ConsumeMessageConcurrentlyService.this.resetRetryTopic(msgs);
                if (msgs != null && !msgs.isEmpty()) {
                    for (MessageExt msg : msgs) {
                        MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis()));
                    }
                }
//NO.3
                status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);
            } catch (Throwable e) {
                log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
                    RemotingHelper.exceptionSimpleDesc(e),
                    ConsumeMessageConcurrentlyService.this.consumerGroup,
                    msgs,
                    messageQueue);
                hasException = true;
            }
            long consumeRT = System.currentTimeMillis() - beginTimestamp;            

...省略代码...

            if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
                consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name());
            }

//NO.4
            if (null == status) {
                log.warn("consumeMessage return null, Group: {} Msgs: {} MQ: {}",
                    ConsumeMessageConcurrentlyService.this.consumerGroup,
                    msgs,
                    messageQueue);
                status = ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }

            if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
                consumeMessageContext.setStatus(status.toString());
                consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status);

//NO.5                
ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
            }

            ConsumeMessageConcurrentlyService.this.getConsumerStatsManager().incConsumeRT(ConsumeMessageConcurrentlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT);

            if (!processQueue.isDropped()) {

//NO.6
                ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
            } else {
                log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs);
            }
        }

NO.1  获得我们注入的回调函数 MessageListenerConcurrently

NO.2 执行钩子函数的before

NO.3 执行回调函数的 consumeMessage 方法

NO.4 判断回调函数返回的状态,如果是空则设置为 ConsumeConcurrentlyStatus.RECONSUME_LATER;

NO.5 执行钩子函数的after方法

NO.6 处理执行结果(消费异常ack与消费进度管理)

消费异常ack与本地消费进度保存核心方法是:

org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService#processConsumeResult

这里主要通过变量 

int ackIndex = context.getAckIndex(); 的设置来处理是否将消息回送到broker;大致逻辑是如果是中正常消费结束 ackindex会被设置成消息大size-1,那么后面代码不会进入循环进而不会将消息发送回broker;如果有异常就会将对应的消息通过retry的形式发回broker,如果发送失败则消费端会再次消费之前被标记失败的消息;
switch (this.defaultMQPushConsumer.getMessageModel()) {
            case BROADCASTING:
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
                    MessageExt msg = consumeRequest.getMsgs().get(i);
                    log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
                }
                break;
            case CLUSTERING:
                List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
                    MessageExt msg = consumeRequest.getMsgs().get(i);
//NO.1
                    boolean result = this.sendMessageBack(msg, context);
                    if (!result) {
                        msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
                        msgBackFailed.add(msg);
                    }
                }

                if (!msgBackFailed.isEmpty()) {
//NO.2
                    consumeRequest.getMsgs().removeAll(msgBackFailed);
//NO.3
                    this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
                }
                break;
            default:
                break;
        }
//NO.4
        long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
        if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
//NO.5            
this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
        }

NO.1 将消费失败的消息发回broker,发送消息的cmd是:RequestCode.CONSUMER_SEND_MSG_BACK

NO.2 将发回失败的消息remove掉

NO.3 如果1过程失败,client会再次重试消费该消息

NO.4 这里会获得最小的一个已被确认消费消息的偏移量(这个方法有可能会引起集群模式下的重复消费问题,比如一个批次有消息m1、m2、m3,如果m1和m3被正常消费了,这个函数返回的偏移量会是m1的偏移量而不是m3的,所以自定义程序要保证程序幂等)

NO.5 将第四步的消费进度返回值存入本地缓存中

消费进度推送broker

在broker启动的时候会将该定时任务启动,主要逻辑是消费进度管理类:RemoteBrokerOffsetStore

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#start()
↓
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#start(boolean)
↓
org.apache.rocketmq.client.impl.factory.MQClientInstance#start
↓
org.apache.rocketmq.client.impl.factory.MQClientInstance#startScheduledTask(创建定时任务)
↓
org.apache.rocketmq.client.impl.factory.MQClientInstance#persistAllConsumerOffset
↓
org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#persistConsumerOffset
↓
org.apache.rocketmq.client.MQHelper#resetOffsetByTimestamp(org.apache.rocketmq.common.protocol.heartbeat.MessageModel, java.lang.String, java.lang.String, java.lang.String, long)
↓
org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#persistAll
    @Override
    public void persistAll(Set<MessageQueue> mqs) {
        if (null == mqs || mqs.isEmpty())
            return;

        final HashSet<MessageQueue> unusedMQ = new HashSet<MessageQueue>();
        if (!mqs.isEmpty()) {
            for (Map.Entry<MessageQueue, AtomicLong> entry : this.offsetTable.entrySet()) {
                MessageQueue mq = entry.getKey();
                AtomicLong offset = entry.getValue();
                if (offset != null) {
                    if (mqs.contains(mq)) {
                        try {
//NO.1
                            this.updateConsumeOffsetToBroker(mq, offset.get());
                            log.info("[persistAll] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}",
                                this.groupName,
                                this.mQClientFactory.getClientId(),
                                mq,
                                offset.get());
                        } catch (Exception e) {
                            log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e);
                        }
                    } else {
                        unusedMQ.add(mq);
                    }
                }
            }
        }

        if (!unusedMQ.isEmpty()) {
            for (MessageQueue mq : unusedMQ) {
                this.offsetTable.remove(mq);
                log.info("remove unused mq, {}, {}", mq, this.groupName);
            }
        }
    }

 NO.1 这里会将client端缓存的消费进度同步到broker端,cmd是  RequestCode.UPDATE_CONSUMER_OFFSET

broker会根据上传的offset做对应的更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值