前言

上文中我们介绍了RocketMQ的顺序消息,顺序消息的客户端消费原理,深入解析了RocketMq是如何保证全局顺序性的,今天聊一下平时用的最多的普通消息,

ConsumeMessageConcurrentlyService

这个类就是我们今天讲的源码主入口了,首先看下

public ConsumeMessageConcurrentlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl,
        MessageListenerConcurrently messageListener) {
				// 代码省略。。。。
  			// 普通消息消费线程池。
        this.consumeExecutor = new ThreadPoolExecutor(
            this.defaultMQPushConsumer.getConsumeThreadMin(),
            this.defaultMQPushConsumer.getConsumeThreadMax(),
            1000 * 60,
            TimeUnit.MILLISECONDS,
            this.consumeRequestQueue,
            new ThreadFactoryImpl("ConsumeMessageThread_"));
				// 延迟消费的线程池
        this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_"));
  			// 消费过期线程池
        this.cleanExpireMsgExecutors = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("CleanExpireMsgScheduledThread_"));
    }

说明:

  1. 普通消息的消费线程池指的就是处理MQ消息的。
  2. 延迟消费线程池呢,是一个定时线程池,当消费失败的消息投递broker失败的时候,会进入这个定时线程池进行内存级别的重试
  3. 消费过期线程池,当消费者消费的时间过长,就会被认为是消费失败,会重新投递消费,默认消费过期时间是15分钟

ConsumeRequest

这个是客户端消费线程池的线程实现类,里面有实现业务逻辑。

@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;
            }
            // 获取消息监听器
            MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener;
            // 构建消息并发上下文
            ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(messageQueue);
            ConsumeConcurrentlyStatus status = null;
            // 设置重试的topic, topic前面会拼接RETRY_TOPIC
            defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());

            // 如果有前置hook, 则执行
            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();
            // 是否存在异常
            boolean hasException = false;
            // 消息消费类型
            ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
            try {
                // 消息不为空
                if (!msgs.isEmpty()) {
                    for (MessageExt msg : msgs) {
                        // 设置每个消息开始消费的时间,这个时间后续会被过期线程池识别并做相应逻辑处理
                        MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis()));
                    }
                }
                // 消费消息
                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 {
                    // 消费没有异常,但是监听器返回了null, 这种也是不对的
                    returnType = ConsumeReturnType.RETURNNULL;
                }
            } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) {
                // 判断消息是否消费超时。consumeTimeout 默认15,所以是15分支
                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()) {
                // 处理消费结果
                ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
            } else {
                log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs);
            }
        }

步骤说明:

看过顺序消息的文章的朋友其实可以感觉到,普通消息消费的流程比顺序消息简单很多,下面简单的说明一下

  1. 获取注册的消息监听器,重置TOPIC, 设置消息的topic为RETRY_TOPIC开头的。
  2. 设置消息的起始消费时间,用来做消费超时。
  3. 调用监听器进行消费
  4. 处理结果,异常判断,设置消费状态
  5. 处理消费结果。

processConsumeResult

可以看下消费结果处理

public void processConsumeResult(
        final ConsumeConcurrentlyStatus status,
        final ConsumeConcurrentlyContext context,
        final ConsumeRequest consumeRequest
    ) {
        // ack确认的下表,默认是Integer.MAX_VALUE,如果消息监听器里面没有设置的话。
        int ackIndex = context.getAckIndex();

        if (consumeRequest.getMsgs().isEmpty())
            return;
        switch (status) {
            case CONSUME_SUCCESS:
                // 消费成功, 如过ack确认下标大于消息数量,则进行重置
                if (ackIndex >= consumeRequest.getMsgs().size()) {
                    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 ,这样就可以进入下面的哪个重试的for循环了
                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());
                // 注意这里,这个地方使用的是ackIndex作为起始值, 如果消息全部消费成功,那么ackIndex就是consumeRequest.getMsgs().size()
                // 也就是说不会进入这个for循环,也就不存在重新投递的问题
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
                    MessageExt msg = consumeRequest.getMsgs().get(i);
                    // c重新投递到broker, 注意:这个时候投递的topic已经变了,不是原来的topic,而是加了RETRY_TOPIC前缀的
                    boolean result = this.sendMessageBack(msg, context);
                    // 如果投递失败
                    if (!result) {
                        // 更新重试次数,走内存重试消费
                        msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
                        msgBackFailed.add(msg);
                    }
                }

                //  投递broker失败,
                if (!msgBackFailed.isEmpty()) {
                    // 从消费线程消息池里面移除
                    consumeRequest.getMsgs().removeAll(msgBackFailed);
                    // 这些投递失败的消息,继续重试
                    this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
                }
                break;
            default:
                break;
        }
        // 分为两种情况
        //消息消费成功或者重新投递broker成功, 会进行更新offset

        // 如果消费失败并且没有重新投递broker成功,那么consumeRequest.getMsgs()就是空的,offset其实就不会变化,都是最小的offset
        // 更新offset的逻辑等会单独讲解。

        long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
        if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
            this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
        }
    }

步骤说明:

  1. 判断消费状态,是成功还是失败, 设置ackIndex , ack是确认消费成功消息的下标,默认是Integer.MAX_VALUE,如果消息监听器里面没有设置的话。 如果消息消费成功, 则会消费成功, 如过ack确认下标大于消息数量,则进行重置
  2. 消费失败,设置ackIndex=-1
  3. 判断消息模式, 处理消费失败的消息, 我们重点说下集群模式,集群模式下,普通消息在消费失败的时候会进行重试。 重试的时候是有个for循环进行判断,这个地方使用的是ackIndex作为起始值, 如果消息全部消费成功,那么ackIndex就是consumeRequest.getMsgs().size(), 也就是说不会进入这个for循环,也就不存在重新投递的问题
  4. 更新offset
public long removeMessage(final List<MessageExt> msgs) {
        long result = -1;
        final long now = System.currentTimeMillis();
        try {
            // 上锁
            this.lockTreeMap.writeLock().lockInterruptibly();
            this.lastConsumeTimestamp = now;
            try {
                // ProcessQueue里面的消息不为空
                if (!msgTreeMap.isEmpty()) {
                    // 设置最大的offset为本次提交的下标
                    result = this.queueOffsetMax + 1;
                    int removedCnt = 0;
                    for (MessageExt msg : msgs) {
                        // 进行消息移除
                        MessageExt prev = msgTreeMap.remove(msg.getQueueOffset());
                        if (prev != null) {
                            removedCnt--;
                            msgSize.addAndGet(0 - msg.getBody().length);
                        }
                    }
                    msgCount.addAndGet(removedCnt);

                    // 注意这里,如果ProcessQueue里面的消息还没空,那么本次提交的offset就是treeMap里面的最小值
                    // 最小值也就是本次消费消息的下一条消息
                    if (!msgTreeMap.isEmpty()) {
                        result = msgTreeMap.firstKey();
                    }
                }
            } finally {
                this.lockTreeMap.writeLock().unlock();
            }
        } catch (Throwable t) {
            log.error("removeMessage exception", t);
        }

        return result;
    }

消息超时机制

public void start() {
        this.cleanExpireMsgExecutors.scheduleAtFixedRate(new Runnable() {

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

        }, this.defaultMQPushConsumer.getConsumeTimeout(), this.defaultMQPushConsumer.getConsumeTimeout(), TimeUnit.MINUTES);
    }

上文我们聊过的消息超时线程池,这个线程池默认是来处理消费时间超过15分钟的,cleanExpireMsg 我们看下这个方法。

cleanExpireMsg
private void cleanExpireMsg() {
        Iterator<Map.Entry<MessageQueue, ProcessQueue>> it =
            this.defaultMQPushConsumerImpl.getRebalanceImpl().getProcessQueueTable().entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<MessageQueue, ProcessQueue> next = it.next();
            ProcessQueue pq = next.getValue();
            pq.cleanExpiredMsg(this.defaultMQPushConsumer);
        }
    }

循环负载到当前客户端的每个队列,每个队列里面存储了消息,处理超时的消息

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 {
                    // 判断msgTreeMap里面的首个元素消费时间是否大约15分钟,如果大于则取出来
                    if (!msgTreeMap.isEmpty() && System.currentTimeMillis() - Long.parseLong(MessageAccessor.getConsumeStartTimeStamp(msgTreeMap.firstEntry().getValue())) > pushConsumer.getConsumeTimeout() * 60 * 1000) {
                      	// 获取
                        msg = msgTreeMap.firstEntry().getValue();
                    } else {
												// 如果不大于,则直接跳过这16个。作者这样设计可能是想加快处理进度,避免循环次数过多而影响性能
                        break;
                    }
                } finally {
                    this.lockTreeMap.readLock().unlock();
                }
            } catch (InterruptedException e) {
                log.error("getExpiredMsg exception", e);
            }

            try {
								// 讲消息进行重试
                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 {
                        if (!msgTreeMap.isEmpty() && msg.getQueueOffset() == msgTreeMap.firstKey()) {
                            try {
                              // 移除消息
                                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);
            }
        }
    }

步骤说明:

  1. 判断是否为顺序消息,如果是则直接跳过
  2. 以16为一个批次来进行处理,加快处理进度,判断msgTreeMap里面的首个元素消费时间是否大约15分钟,如果大于则取出来进行重试
  3. 进行重试
  4. 移除消息