RocketMQ源码学习 (十二) 消费者-并发消费服务源码学习

接下来根据上篇文章所讲述的并发消费服务的原理进行本文的源码学习。

1 ConsumeMessageConcurrentlyService 类的主要内容


public class ConsumeMessageConcurrentlyService implements ConsumeMessageService {
    private static final InternalLogger log = ClientLogger.getLog();
    // 消费者实现对象
    private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
    // 消费者门面对象(Config)
    private final DefaultMQPushConsumer defaultMQPushConsumer;
    // 消息监听器(消息处理的逻辑在此封装,该messageListener由开发者自己实现,并注册到 defaultMQPushConsumer)
    private final MessageListenerConcurrently messageListener;

    // 消费任务队列
    private final BlockingQueue<Runnable> consumeRequestQueue;
    // 消费任务线程池
    private final ThreadPoolExecutor consumeExecutor;
    // 消费者组
    private final String consumerGroup;

    // 调度线程池(用途:延迟提交消费任务)
    private final ScheduledExecutorService scheduledExecutorService;

    // 运行“清理过期消息任务”的调度线程池(该任务15min执行一次)
    private final ScheduledExecutorService cleanExpireMsgExecutors;
    //...省略
    public void start() {
        // 提交“清理过期消息任务”任务,延迟15min之后执行,之后每15min执行一次
        this.cleanExpireMsgExecutors.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                cleanExpireMsg();
            }

        }, this.defaultMQPushConsumer.getConsumeTimeout(), this.defaultMQPushConsumer.getConsumeTimeout(), TimeUnit.MINUTES);
    }
    //...省略
    private void cleanExpireMsg() {
        // processQueueTable 存储的是分配给当前消费者的 队列(mq,pq)
        Iterator<Map.Entry<MessageQueue, ProcessQueue>> it =
            this.defaultMQPushConsumerImpl.getRebalanceImpl().getProcessQueueTable().entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<MessageQueue, ProcessQueue> next = it.next();
            // 获取该messageQueue在消费者本地的 processQueue
            ProcessQueue pq = next.getValue();
            // 调用pq 清理过期消息的方法
            pq.cleanExpiredMsg(this.defaultMQPushConsumer);
        }
    }
//省略
    /**
     * 提交消费任务方法
     * @param msgs (一般情况:从服务器 pull 下来的这批消息)
     * @param processQueue (消息归属mq在消费者端的processQueue,注意,提交消费任务之前,msgs已经加入到该pq内了..)
     * @param messageQueue 消息归属队列
     * @param dispatchToConsume 并发消息此参数无效
     */
    @Override
    public void submitConsumeRequest(
        final List<MessageExt> msgs,
        final ProcessQueue processQueue,
        final MessageQueue messageQueue,
        final boolean dispatchToConsume) {
        // 此参数控制 一个消费任务 可消费的消息数量 (默认:1)
        final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();

        if (msgs.size() <= consumeBatchSize) {
            // CASE: msgs 内部的消息数 是小于等于 consumeBatchSize 值的,那直接封装一个 “消费任务” 提交到消费线程池即可。
            ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
            try {
                this.consumeExecutor.submit(consumeRequest);
            } catch (RejectedExecutionException e) {
                this.submitConsumeRequestLater(consumeRequest);
            }
        } else {
            // CASE:msgs 消息数 > consumeBatchSize,这里逻辑 就是按照 consumeBatchSize 规则 将 msgs 拆分成多个 “消费任务” 提交到 消费线程池。
            for (int total = 0; total < msgs.size(); ) {
                List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize);
                for (int i = 0; i < consumeBatchSize; i++, total++) {
                    if (total < msgs.size()) {
                        msgThis.add(msgs.get(total));
                    } else {
                        break;
                    }
                }

                ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);
                try {
                    this.consumeExecutor.submit(consumeRequest);
                } catch (RejectedExecutionException e) {
                    for (; total < msgs.size(); total++) {
                        msgThis.add(msgs.get(total));
                    }

                    this.submitConsumeRequestLater(consumeRequest);
                }
            }
        }
    }

}

1.2 cleanExpiredMsg方法清理过期消息

清理过期消息的逻辑中调用了ProcessQueue.cleanExpiredMsg方法


    /**
     * @param pushConsumer
     */
    public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) {
        if (pushConsumer.getDefaultMQPushConsumerImpl().isConsumeOrderly()) {
            // 顺序消费的话,不会执行清理过期消息的逻辑。
            return;
        }
        // 最多循环16次
        int loop = msgTreeMap.size() < 16 ? msgTreeMap.size() : 16;

        for (int i = 0; i < loop; i++) {
            MessageExt msg = null;
            try {
                this.lockTreeMap.readLock().lockInterruptibly();
                try {
                    if (!msgTreeMap.isEmpty() && System.currentTimeMillis() - Long.parseLong(MessageAccessor.getConsumeStartTimeStamp(msgTreeMap.firstEntry().getValue())) > pushConsumer.getConsumeTimeout() * 60 * 1000) {
                        // treeMap中如果第一条消息 它的消费开始时间 与 系统时间 差值 > 15min,
                        // 则取出该消息(注意:这一步 并没有将 entry 从 treeMap 移除)
                        msg = msgTreeMap.firstEntry().getValue();
                    } else {
                        // 为什么从这里跳出去呢?
                        // 快照队列内的消息 是有顺序的
                        // a b c d e f g 这些消息
                        // a 不是过期的,b cd e... 这些消息肯定都不是过期的
                        break;
                    }
                } finally {
                    this.lockTreeMap.readLock().unlock();
                }
            } catch (InterruptedException e) {
                log.error("getExpiredMsg exception", e);
            }

            try {
                // 消息回退到服务器,设置该消息的 延迟级别为 3
                pushConsumer.sendMessageBack(msg, 3);
                log.info("send expire msg back. topic={}, msgId={}, storeHost={}, queueId={}, queueOffset={}", msg.getTopic(), msg.getMsgId(), msg.getStoreHost(), msg.getQueueId(), msg.getQueueOffset());
                try {
                    this.lockTreeMap.writeLock().lockInterruptibly();
                    try {
                        // 条件不成立:说明在消息回退期间,消费任务 将 目标“消息”成功消费了,成功消费后,消费任务会执行 processConsumeResult
                        // 在这一步 会将 目标“msg”从 msgTreeMap 移除.. 就会导致 msg.getQueueOffset() == msgTreeMap.firstKey() =》 false

                        // 条件成立:消息回退期间,该目标“消息”并没有被 消费任务 成功消费
                        if (!msgTreeMap.isEmpty() && msg.getQueueOffset() == msgTreeMap.firstKey()) {
                            try {
                                // 从treeMap 将 该 回退成功的 msg 删除
                                removeMessage(Collections.singletonList(msg));
                            } catch (Exception e) {
                                log.error("send expired msg exception", e);
                            }
                        }
                    } finally {
                        this.lockTreeMap.writeLock().unlock();
                    }
                } catch (InterruptedException e) {
                    log.error("getExpiredMsg exception", e);
                }
            } catch (Exception e) {
                log.error("send expired msg exception", e);
            }
        }
    }

2 ConsumeRequest内部类和run方法

在 ConsumeMessageConcurrentlyService 类中,包含了内部类ConsumeRequest ,该类包含了run方法,消费逻辑的核心。

class ConsumeRequest implements Runnable {
        // 分配到该消费任务的消息
        private final List<MessageExt> msgs;
        // 消息处理队列
        private final ProcessQueue processQueue;
        // 消息队列
        private final MessageQueue messageQueue;
        public ConsumeRequest(List<MessageExt> msgs, ProcessQueue processQueue, MessageQueue messageQueue) {
            this.msgs = msgs;
            this.processQueue = processQueue;
            this.messageQueue = messageQueue;
        }
        public List<MessageExt> getMsgs() {
            return msgs;
        }
        public ProcessQueue getProcessQueue() {
            return processQueue;
        }
        @Override
        public void run() {
            // 条件成立:说明该queue经过 rbl 分配到其它consumer,当前consumer不需要再去消费该queue的消息了..直接返回
            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;
            }
            
            // 获取 messageListener (开发者创建的“消息监听器”,消息处理的业务逻辑都在此封装)
            MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener;

            // 消费上下文对象
		    /**
		     * Message consume retry strategy<br>
		     * -1,no retry,put into DLQ directly<br>  死信
		     * 0,broker control retry frequency<br>   服务端控制延迟级别
		     * >0,client control retry frequency      客户端控制延迟级别
		     */
            ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(messageQueue);

            // 消费状态(CONSUME_SUCCESS / RECONSUME_LATER)
            ConsumeConcurrentlyStatus status = null;

            // 参数1:待消息的消息
            // 参数2:消费者组
            defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());
            
            ConsumeMessageContext consumeMessageContext = null;
            if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
                consumeMessageContext = new ConsumeMessageContext();
                consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());
                consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup());
                consumeMessageContext.setProps(new HashMap<String, String>());
                consumeMessageContext.setMq(messageQueue);
                consumeMessageContext.setMsgList(msgs);
                consumeMessageContext.setSuccess(false);
                ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
            }

            // 消费开始时间
            long beginTimestamp = System.currentTimeMillis();
            // 消费过程中 是否 listener是否向外抛出异常
            boolean hasException = false;
            ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
            try {
                if (msgs != null && !msgs.isEmpty()) {

                    for (MessageExt msg : msgs) {
                        // 给每条消息设置 消费开始时间 (清理过期消息的任务,会检查该属性..判断是否消费超时..)
                        MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis()));
                    }
                }
                // 参数1:Collections.unmodifiableList(msgs) 创建的list内容 还是 msgs 这些消息,但是该list 不能被添加 或者 删除 数据..
                // 参数2:消费上下文(主要是控制 消息消费失败时,消息延迟级别)
                // 返回值:(CONSUME_SUCCESS / RECONSUME_LATER / NULL)
                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 (null == status) {
                if (hasException) {
                    returnType = ConsumeReturnType.EXCEPTION;
                } else {
                    returnType = ConsumeReturnType.RETURNNULL;
                }
            } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) {
                returnType = ConsumeReturnType.TIME_OUT;
            } else if (ConsumeConcurrentlyStatus.RECONSUME_LATER == status) {
                returnType = ConsumeReturnType.FAILED;
            } else if (ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status) {
                returnType = ConsumeReturnType.SUCCESS;
            }
            if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
                consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name());
            }
            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);
                ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
            }
            ConsumeMessageConcurrentlyService.this.getConsumerStatsManager()
                .incConsumeRT(ConsumeMessageConcurrentlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT);
            
            if (!processQueue.isDropped()) {
                // 正常走这里

                // 处理消费结果,有什么事需要处理呢?
                // 1. 消费成功的话,需要将msgs从 pq 移除
                // 2. 消费失败的话,需要将消费失败的消息 回退 给 服务器,并且将回退失败的消息(会将回退失败的消息从当前任务移除) 再次提交 消费任务,
                // 最后也会将 CR.msgs 从pq移除
                // 3. 更新消费进度

                // 参数1:消费结果状态
                // 参数2:消费上下文
                // 参数3:当前消费任务
                ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
            } else {
                // 执行到这里,说明 messageListener 运行过程中,该mq分配到其它 consumer 或者 当前消费者退出...
                log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs);
            }
        }

        public MessageQueue getMessageQueue() {
            return messageQueue;
        }

    }

2.1 resetRetryAndNamespace方法

如果消息来自延迟队列则设置其topic为%RETRY_TOPIC%+consumerGroup。


    // 参数1:待消息的消息
    // 参数2:消费者组
    public void resetRetryAndNamespace(final List<MessageExt> msgs, String consumerGroup) {
        // 获取当前消费者组的 重试主题:%RETRY%GroupName
        final String groupTopic = MixAll.getRetryTopic(consumerGroup);

        for (MessageExt msg : msgs) {

            // 被重试的主题,原主题 (一般消息 没有该属性,只有 被重复消费的消息 才有该属性)
            String retryTopic = msg.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);

            // 条件成立:说明该“消息”是被重复消费的消息
            if (retryTopic != null && groupTopic.equals(msg.getTopic())) {
                // 将被重复消费的消息 主题 修改回  “原主题”
                msg.setTopic(retryTopic);
            }

            if (StringUtils.isNotEmpty(this.defaultMQPushConsumer.getNamespace())) {
                msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace()));
            }
        }
    }

3 processConsumeResult方法处理消费结果

在run方法的最后,调用了processConsumeResult方法。


    // 参数1:消费结果状态
    // 参数2:消费上下文
    // 参数3:当前消费任务
    public void processConsumeResult(
        final ConsumeConcurrentlyStatus status,
        final ConsumeConcurrentlyContext context,
        final ConsumeRequest consumeRequest
    ) {

        int ackIndex = context.getAckIndex();

        if (consumeRequest.getMsgs().isEmpty())
            return;

        switch (status) {
            case CONSUME_SUCCESS:
                if (ackIndex >= consumeRequest.getMsgs().size()) {
                    // 消费成功的话,ackIndex 设置 成 消费消息数 -1 的值
                    // 举个例子: 假设 msgs 内有 10 条消息,那么 ackIndex = 9
                    ackIndex = consumeRequest.getMsgs().size() - 1;
                }
                int ok = ackIndex + 1;
                int failed = consumeRequest.getMsgs().size() - ok;
                this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), ok);
                this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed);
                break;
            case RECONSUME_LATER:// 消费失败..
                ackIndex = -1;
                this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(),
                    consumeRequest.getMsgs().size());
                break;
            default:
                break;
        }

        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);
                    boolean result = this.sendMessageBack(msg, context);
                    if (!result) {
                        // 回退失败的情况

                        // 将消息重试属性 ++
                        msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
                        // 加入到回退失败 集合
                        msgBackFailed.add(msg);
                    }
                }

                if (!msgBackFailed.isEmpty()) {
                    // 将 回退失败的消息 从 当前消费任务的 msgs 集合内 移除
                    consumeRequest.getMsgs().removeAll(msgBackFailed);

                    // 对于回退失败的消息,再次提交消费任务,延迟5秒钟后 再次尝试消费
                    this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
                }
                break;
            default:
                break;
        }


        // 从 pq 中删除已经消费成功的消息,返回offset
        // 返回值:表示pq本地的消费进度 【1. -1 说明 pq 内无数据  2. queueOffsetMax + 1(删完这批msgs之后 无消息了)
        // 3. 删除完该批msgs之后 pq内 还有剩余待消费的消息,此时返回 firstMsg offset】
        long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());


        if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {

            // 更新消费者本地的该mq的消费进度

            // 参数1:mq
            // 参数2:offset (更新值)
            // 参数3:increaseOnly (true)  消费进度不能 逆增长..所以 这个值传 true
            this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值