【RocketMQ】消息重试、重试次数设置、死信队列

1. 死信队列

上一篇【RocketMQ】消息重试中我们提到当一条消息消费失败时,RocketMQ会进行一定次数的重试。重试的结果也很简单,无非就是在第N次重试时,被成功消费。或者就是经过M次重试后,仍然没有被成功消费。这通常是由于消费者在正常情况下无法正确地消费该消息。此时,RocketMQ不会立即将消息丢弃,而是将其发送到该消费者对应的特殊队列中去。

在RocketMQ中,这种正常情况下无法被消费的消息被称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。

1.1 死信特性

(1)死信消息具有以下特性:

  • 不会再被消费者正常消费。
  • 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。

(2)死信队列具有以下特性:

  • 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
  • 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
  • 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。

1.2 查看死信消息

(1)在控制条查询出现死信队列的主题信息
在这里插入图片描述

(2)在消费界面根据主题查询死信消息
在这里插入图片描述
(3)选择重新发送消息

一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息。因此,通常需要我们对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。

2.重试次数参数

RocketMQ的重试机制涉及发送端重试和消费端重试,消费端重试关联死信队列

2.1 Producer端重试

生产者端的消息失败,也就是Producer往MQ上发消息没有发送成功,比如网络抖动导致生产者发送消息到MQ失败。

这种消息失败重试我们可以手动设置发送失败重试的次数,看一下代码:

public class DefaultMQProducer  {
	//设置消息发送失败时的最大重试次数
	public void setRetryTimesWhenSendFailed(int retryTimesWhenSendFailed) {
	   this.retryTimesWhenSendFailed = retryTimesWhenSendFailed;
	}

在这里插入图片描述

2.2 Consumer端重试

注:只有在消息模式为MessageModel.CLUSTERING集群模式时,Broker才会自动进行重试,广播消息是不会重试的。

消费者消费消息后,需要给Broker返回消费状态。以MessageListenerConcurrently监听器为例,Consumer消费完成后需要返回ConsumeConcurrentlyStatus并发消费状态。查看源码,ConsumeConcurrentlyStatus是一个枚举,共有两种状态:

public enum ConsumeConcurrentlyStatus {
   //消费成功
   ConsumeConcurrentlyStatus,

   //消费失败,一段时间后重试
   RECONSUME_LATER;
}

Consumer端的重试包括两种情况

  • 异常重试:由于Consumer端逻辑出现了异常,导致返回了RECONSUME_LATER状态,那么Broker就会在一段时间后尝试重试。
  • 超时重试:如果Consumer端处理时间过长,或者由于某些原因线程挂起,导致迟迟没有返回消费状态,Broker就会认为Consumer消费超时,此时会发起超时重试。

因此,如果Consumer端正常消费成功,一定要返回ConsumeConcurrentlyStatus.ConsumeConcurrentlyStatus状态。

3.1 异常重试

RocketMQ可在broker.conf文件中配置Consumer端的重试次数和重试时间间隔,如下:

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

默认18时间间隔,表示重试18次,可以减少配置的数量吗?确切的说异常重试复用了延迟队列,因为如果失败了,立即重试,往往还是失败的,例如网络暂时中断,这样通过不断增加重试时间间隔,第一次失败,丢进1s的队列,第二次丢进5s的队列。利用延迟队列,保证了可以多次重试,并通过延迟时间确保业务尽量能成功。

如果用户没有配置,在有默认值(broker侧源码,不是客户端的源码),以rocketmq-all-4.7.1-source-release.zip为例(https://gitee.com/king_beijixiong/study-rocketmq/blob/master/rocketmq-all-4.7.1-source-release.zip):

public class MessageStoreConfig {
   private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
package org.apache.rocketmq.example.quickstart;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.Date;
import java.util.List;

public class Consumer {

    public static void main(String[] args) throws InterruptedException, MQClientException {

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");

        consumer.setNamesrvAddr("10.89.184.62:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

        consumer.subscribe("TopicTest2", "*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {

            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {

                System.out.println("date="+new Date()+" *******");

                for(MessageExt msg :msgs){
                    System.out.println("msg="+msg.getMsgId());
                    System.out.println("date="+new Date());
                    System.out.println("ReconsumeTimes="+msg.getReconsumeTimes());
                    System.out.println();
                }
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                //  return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });


        consumer.start();

        System.out.printf("Consumer Started.%n");
    }
}

执行结果:

onsumer Started.
date=Fri Aug 05 14:08:52 CST 2022 *******
msg=0A28A4923EC018B4AAC217A272330000
date=Fri Aug 05 14:08:52 CST 2022
ReconsumeTimes=0                        '第一次处理'

date=Fri Aug 05 14:09:02 CST 2022 *******
msg=0A28A4923EC018B4AAC217A272330000
date=Fri Aug 05 14:09:02 CST 2022
ReconsumeTimes=1                       '第2次处理 与第一次间隔10s'

date=Fri Aug 05 14:09:33 CST 2022 *******
msg=0A28A4923EC018B4AAC217A272330000
date=Fri Aug 05 14:09:33 CST 2022
ReconsumeTimes=2						'第3次处理 与第2次间隔20s'

date=Fri Aug 05 14:10:33 CST 2022 *******
msg=0A28A4923EC018B4AAC217A272330000
date=Fri Aug 05 14:10:33 CST 2022
ReconsumeTimes=3                       '第4次处理 与第3次间隔1m'


.....后面还有,省略

从结果看,失败后会重试,并且每次间隔时间符合messageDelayLevel规律,当然跳过了1和5s,是从第三个开始的。

但是在大部分情况下,如果Consumer端逻辑出现异常,重试太多次也没有很大的意义,我们可以在代码中指定最大的重试次数。如下:

即提前返回成功状态,是假的成功,也不会进入死信队列。反之,捕获异常,每次返回RECONSUME_LATER,到18次就会进入死信队列

package william.rmq.consumer.quickstart;

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import william.rmq.common.constant.RocketMQConstant;

import javax.annotation.PostConstruct;
import java.util.List;

/**

* @Description:RocketMQ消息消费者
*/
@Slf4j
@Service
public class MessageConsumer implements MessageListenerConcurrently {
   @Value("${spring.rocketmq.namesrvAddr}")
   private String namesrvAddr;

   private final DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("DefaultConsumer");


   @PostConstruct
   public void start() {
       try {
           consumer.setNamesrvAddr(namesrvAddr);

           //从消息队列头部开始消费
           consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

           //设置集群消费模式
           consumer.setMessageModel(MessageModel.CLUSTERING);

           //订阅主题
           consumer.subscribe("DefaultCluster", "*");

           //注册消息监听器
           consumer.registerMessageListener(this);

           //启动消费端
           consumer.start();

           log.info("Message Consumer Start...");
           System.err.println("Message Consumer Start...");
       } catch (MQClientException e) {
           log.error("Message Consumer Start Error!!",e);
       }

   }

   @Override
   public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
       if (CollectionUtils.isEmpty(msgs)) {
           return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
       }

       MessageExt message = msgs.get(0);
       try {
           //逐条消费
           String messageBody = new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET);
           System.err.println("Message Consumer: Handle New Message: messageId: " + message.getMsgId() + ",topic: " +
                   message.getTopic() + ",tags: " + message.getTags() + ",messageBody: " + messageBody);

           //模拟业务异常
           int i = 1 / 0;
           return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
       } catch (Exception e) {
           log.error("Consume Message Error!!", e);
           //抛出异常时,返回ConsumeConcurrentlyStatus.RECONSUME_LATER,尝试重试。当重试指定次数后返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS
           int reconsumeTimes = message.getReconsumeTimes();
           System.err.println("Now Retry Times: " + reconsumeTimes);
           if (reconsumeTimes >= RocketMQConstant.MAX_RETRY_TIMES) {
               return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
           }
           return ConsumeConcurrentlyStatus.RECONSUME_LATER;
       }
   }

}

可以看到控制台打印如下:

Now Retry Times: 3
Message Consumer: Handle New Message: messageId: 0A0E096CA14618B4AAC2562C6D5B0000,topic: DefaultCluster,tags: Tags,messageBody: Message-1
Now Retry Times: 3
Message Consumer: Handle New Message: messageId: C0A81FFA7FF318B4AAC24A37C32C0007,topic: DefaultCluster,tags: Tags,messageBody: Order-2-完成
Now Retry Times: 3
Now Retry Times: 3
Message Consumer: Handle New Message: messageId: C0A81FFA7FF318B4AAC24A37C3290006,topic: DefaultCluster,tags: Tags,messageBody: Order-2-支付
Now Retry Times: 3
Now Retry Times: 3

消息重试指定的次数后,就返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS不再重试了。

3.2 超时重试

当Consumer处理时间过长,在超时时间内没有返回给Broker消费状态,那么Broker也会自动重试

package william.rmq.consumer.quickstart;

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import william.rmq.common.constant.RocketMQConstant;

import javax.annotation.PostConstruct;
import java.util.List;

/**
* @Auther: ZhangShenao
* @Date: 2018/9/7 11:06
* @Description:RocketMQ消息消费者
*/
@Slf4j
@Service
public class MessageConsumer implements MessageListenerConcurrently {
   @Value("${spring.rocketmq.namesrvAddr}")
   private String namesrvAddr;

   private final DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("DefaultConsumer");


   @PostConstruct
   public void start() {
       try {
           consumer.setNamesrvAddr(namesrvAddr);

           //从消息队列头部开始消费
           consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

           //设置集群消费模式
           consumer.setMessageModel(MessageModel.CLUSTERING);

           //设置消费超时时间(分钟)
           consumer.setConsumeTimeout(RocketMQConstant.CONSUMER_TIMEOUT_MINUTES);

           //订阅主题
           consumer.subscribe("DefaultCluster", "*");

           //注册消息监听器
           consumer.registerMessageListener(this);

           //启动消费端
           consumer.start();

           log.info("Message Consumer Start...");
           System.err.println("Message Consumer Start...");
       } catch (MQClientException e) {
           log.error("Message Consumer Start Error!!",e);
       }

   }

   @Override
   public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
       if (CollectionUtils.isEmpty(msgs)) {
           return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
       }

       MessageExt message = msgs.get(0);
       try {
           //逐条消费
           String messageBody = new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET);
           System.err.println("Message Consumer: Handle New Message: messageId: " + message.getMsgId() + ",topic: " +
                   message.getTopic() + ",tags: " + message.getTags() + ",messageBody: " + messageBody);

           //模拟耗时操作2分钟,大于设置的消费超时时间
           Thread.sleep(1000L * 60 * 2);
           return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
       } catch (Exception e) {
           log.error("Consume Message Error!!", e);
           return ConsumeConcurrentlyStatus.RECONSUME_LATER;
       }
   }

}

可以看到, Thread.sleep耗时超过最大超时时间,触发失败

参考

RocketMQ:死信队列和消息幂等
RocketMQ详解(12)——RocketMQ的重试机制
源码分析RocketMQ之消息消费重试机制 broker源码,含有16次最大重试次数源码

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
RocketMQ重试队列是指在RocketMQ消息中间件中,当某个消费者无法成功消费一条消息时,RocketMQ会对该消息进行重试,并将其投递到一个特殊的队列中,该队列被称为重试队列。 在RocketMQ的客户端源码中,可以看到对重试机制的实现,通过设置最大重试次数来控制消息重试行为。在DefaultMQPushConsumerImpl.java文件中,可以找到获取最大重试次数的方法getMaxReconsumeTimes(),默认的最大重试次数是16,当达到最大重试次数后,RocketMQ会将消息投递至死信队列。 所以,当消息无法被成功消费时,RocketMQ会将其放入重试队列,进行最大重试次数的尝试。如果仍然无法消费成功,则消息会被投递到死信队列中。对于死信队列中的消息,我们需要关注并进行人工的业务补偿操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [springboot Rabbit死信队列实现,rocketMq重试消息实现](https://download.csdn.net/download/zhengjie01/11041887)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [关于RocketMQ消息重试简述](https://blog.csdn.net/ysds20211402/article/details/124569910)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值