RocketMQ-详解消费者消费失败后重新消费原理

本文基于RocketMQ 4.7.1版本

前面两篇文章介绍了DefaultMQPushConsumer和DefaultMQPullConsumer消费消息的原理,由此我们知道了

  • DefaultMQPushConsumer消费消息相当于全自动,开发人员只需要创建好监听器即可,其他的问题都由rocketmq自动处理;
  • DefaultMQPullConsumer相当于手动,这给开发人员提供了极大的自由度,但也带来了编程上的困难,需要开发人员自己维护消费位移,判断消费失败是否重新消费。

本文主要介绍DefaultMQPushConsumer消费失败后如何自动发起重新消费。

文章目录

  • 一、原理图
  • 二、消费者消费消息原理
  • 三、broker处理重新拉取请求原理
  • 四、消费重试主题
  • 五、死信主题

一、原理图

下图是整个消费过程的原理图,大家可以参考该图阅读本文内容。
RocketMQ-详解消费者消费失败后重新消费原理_第1张图片

二、消费者消费消息原理

从文章《RocketMQ-DefaultMQPushConsumer消费消息原理详解》中可以知道,消息从broker拉取回后,会转发给ConsumeMessageConcurrentlyService处理。
ConsumeMessageConcurrentlyService内部启动一个线程池异步处理消息,线程池最多可以有20个线程。每个消息放入到线程池之前,会使用ConsumeRequest进行封装,代码如下:

//msgs:拉取回的消息集合
//processQueue:是ProcessQueue对象,用于记录队列统计信息,锁,队列状态,临时存储拉取的消息,相当于队列的状态对象
//messageQueue:记录了主题名、队列号、borker名字
ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);

ConsumeRequest实现了Runnable接口,放入线程池后线程池执行其run方法。下面看一下run()方法的执行逻辑:

  1. 执行回调钩子ConsumeMessageHook的executeHookBefore方法;
  2. 调用自定义的业务逻辑监听器,监听器可能抛出异常,也可以返回成功(CONSUME_SUCCESS)或者稍后消费(RECONSUME_LATER);
  3. 如果抛出了异常,rocketmq修改变量值,认为监听器返回了稍后消费(RECONSUME_LATER),因此消费抛出异常或者返回RECONSUME_LATER,都视为消费失败,稍后重试;
  4. 执行回调钩子ConsumeMessageHook的executeHookAfter方法;
  5. 统计消费成功或者失败信息;
  6. 如果消费失败,则发出消费失败的请求,在请求参数里面携带了消费失败消息的位移;
  7. 最后无论消费成功还是失败都更新本地的提交位移。

从上面的执行逻辑可以看出,监听器抛出异常或者返回RECONSUME_LATER,都属于消费失败,rocketmq对它们的处理是一样的。这里还有一点要注意,消费失败的请求对象与正常拉取消息的请求对象是不同的:

  • ConsumerSendMsgBackRequestHeader用于告知broker消费失败,broker只处理请求参数中指定的消息;
  • PullMessageRequestHeader用于正常拉取消息,可以拉取指定消费位移后的一条或者多条消息。

下面看一下ConsumerSendMsgBackRequestHeader的请求参数有哪些:

        //设置消费组
        requestHeader.setGroup(consumerGroup);
        //设置原始主题
        requestHeader.setOriginTopic(msg.getTopic());
        //设置消费位移,这个消费位移是物理位移
        requestHeader.setOffset(msg.getCommitLogOffset());
        //设置重试策略,可以设置-1,0,大于0,默认是0,表示由broker控制
        requestHeader.setDelayLevel(delayLevel);
        //设置消息的id
        requestHeader.setOriginMsgId(msg.getMsgId());
        //设置重试次数,默认是16次
        requestHeader.setMaxReconsumeTimes(maxConsumeRetryTimes);

ConsumerSendMsgBackRequestHeader请求发送到broker,下面来看broker如何处理该请求。

三、broker处理重新拉取请求原理

broker收到请求后,将请求转发给SendMessageProcessor.asyncConsumerSendMsgBack()方法。该方法首先进行校验,比如权限、是否有订阅组信息等。校验通过后,根据请求参数中的group值,也就是消费组,生成重试主题名:%RETRY%消费组名。默认情况下,重试主题有一个读队列和一个写队列。
之后根据请求参数中的位移,找到对应的消息,从消息中解析出当前的重试次数和消息体内容,如果是第一次重试,那么找到的消息其实就是原始消费失败的消息,自然得到的重试次数是0,之后将重试次数与允许的最大重试次数做比较:

  • 如果小于最大重试次数,则创建一个消息,消息体内容为原始消息体内容,然后将该消息写入到定时主题(SCHEDULE_TOPIC_XXXX)中,同时写入的还有重试次数,真实主题(%RETRY%消费组名)和真实队列,此时写入的重试次数是在原重试次数的基础上+1得到的。消息写入成功后返回消费者成功。
  • 如果大于等于最大重试次数,则将消息写入死信队列,这个文章后面再详细介绍。

从上面看到,重试消息没有写入到重试主题中,而是写入到了定时主题,也就是SCHEDULE_TOPIC_XXXX中。这是因为,消费者消费失败,一般需要延迟一段时间再重试,定时主题就是起这个作用,broker根据重试次数计算一个延迟级别:

if (0 == delayLevel) {
	delayLevel = 3 + msgExt.getReconsumeTimes();//在重试次数的基础上+3
}
msgExt.setDelayTimeLevel(delayLevel);

延迟级别越大,延迟时间就越长。rocketmq定义了18个级别,延迟时间分别为(参见MessageStoreConfig.messageDelayLevel属性):

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

broker上有一个服务ScheduleMessageService,该服务启动一个Timer定时器,定时读取每个定时队列里面的消息。
到了定时队列的读取时间后,rocketmq取出队列里面的消息,根据消息里面记录的真实队列和真实主题,创建一个新的消息,将消息写入到真实队列和真实主题中,其实也就是%RETRY%消费组名中。

四、消费重试主题

通过前面两小节内容可以知道,消息消费失败了,消费者发送ConsumerSendMsgBackRequestHeader请求到broker,broker根据请求创建一个消息,并最终将消息写入重试队列。到这里,大家是否感到奇怪,broker为什么没有将消费失败的消息再返回给消费者,而是写入到了重试主题中?
这时候rocketmq的再平衡服务又要开始发挥作用了。在启动的时候,消费者根据消费组名生成对应的重试主题名,并将该主题加入到再平衡服务中,之后再平衡服务遍历主题,生成一个PullRequest拉取请求对象,PullMessageService服务发现PullRequest请求后,根据该请求创建PullMessageRequestHeader对象,向broker拉取重试主题的消息,拉取回后,对消息再次消费。

这里对前面三节的内容做一下总结:

  1. 消费者消费失败消息,会发送一个消费失败的消息到broker;
  2. broker收到消费失败的消息,将该消息写入到定时队列中;
  3. 延迟一段时间后,从定时队列中取出消息然后将消息写入到重试主题中;
  4. 消费者的再平衡服务构建一个请求对象,请求拉取重试主题的消息;
  5. broker收到请求后,取出重试主题的消息返回给消费者;
  6. 消费者再次消费之前失败的消息。

五、死信主题

之前提到重试次数超过16次,消息会被放入死信主题。
死信主题是用于存储一些无法成功消费的消息,当消息重试次数超过最大重试次数,就会被放入该主题中。
SendMessageProcessor.asyncConsumerSendMsgBack()每次收到消息失败的消息后,都会根据消费位移取出消息,得到消息的重试次数,将该次数与最大重试次数比较:

        //如果超过了最大重试次数,将消息放入死信主题
        //maxReconsumeTimes表示最大重试次数
        if (msgExt.getReconsumeTimes() >= maxReconsumeTimes 
            || delayLevel < 0) {
            //生成死信主题名:%DLQ%消费组名
            newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
            //死信主题只有一个队列
            queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP;
            //创建死信主题配置对象,如果没有死信主题,则创建
            topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
                    DLQ_NUMS_PER_GROUP,
                    PermName.PERM_WRITE, 0);
            //如果死信主题创建失败,返回请求方失败信息
            if (null == topicConfig) {
                response.setCode(ResponseCode.SYSTEM_ERROR);
                response.setRemark("topic[" + newTopic + "] not exist");
                return CompletableFuture.completedFuture(response);
            }
        }

之后broker将创建一个消息,并将该消息写入到死信主题中,其中消息的消息体为消费失败的消息体。
死信主题中的消息,默认情况下是不会自动消费的,需要开发人员编写程序读取其中的消息或者从rocketmq提供监控台上查看。

你可能感兴趣的:(rocketMQ,rocketmq,死信队列,重试队列,重试主题,死信主题)