rabbitmq失败重试机制+死信队列,防止消息未被消费

如何保证消息一定被消费掉?请看下面三步骤:

一、重试机制

首先说一下RabbitMQ的消息重试机制,顾名思义,就是消息消费失败后进行重试,重试机制的触发条件是消费者显式的抛出异常,这个很类似@Transactional,如果没有显式地抛出异常或者try catch起来没有手动回滚,事务是不会回滚的。

相关配置如下:

spring:
  rabbitmq:
    host: cl-cloud-rabbitmq
    username: swh
    password: swh123
    port: 5672
    publisher-confirms: true
    publisher-returns: true
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual
        #消费者的最小数量
        concurrency: 1
        #消费者的最大数量
        max-concurrency: 5
        retry:
          #是否支持重试
          enabled: true
          #最大重试次数
          max-attempts: 12
          #最大间隔时间
          max-interval: 3600000
          #初始重试间隔时间
          initial-interval: 1000
          #乘子  重试间隔*乘子得出下次重试间隔  3s  9s  27s
          multiplier: 3
        #重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
        default-requeue-rejected: false

二、死信队列

死信就是消息在特定场景下的一种表现形式,这些场景包括:

  •   消息被拒绝(basic.reject / basic.nack),并且requeue = false
  •   消息的 TTL 过期时
  •   消息队列达到最大长度
  •   达到最大重试限制

死信队列就是用于储存死信的消息队列,在死信队列中,有且只有死信构成,不会存在其余类型的消息。死信队列也是一个普通队列,也可以被消费者消费,区别在于业务队列需要绑定在死信队列上,才能正常地把死信发送到死信队列上。

死信队列实现:

  1)创建死信交换机

/**
 * 延迟交换器构造器
 *
 * @author: zyf
 * @date: 2019/3/8 13:31
 * @description:
 */
public class ExchangeBuilder {
    /**
     * 默认延迟消息交换器
     */
    public final static String DEFAULT_DELAYED_EXCHANGE = "cl.delayed.exchange";
    /**
     * 普通交换器
     */
    public final static String DEFAULT_DIRECT_EXCHANGE = "cl.direct.exchange";
    /**
     * 死信交换器
     */
    public final static String DEAD_EXCHANGE = "cl.exchange.dead";
    /**
     * 死信交换机绑定ke
     */
    public final static String DEAD_EXCHANGE_ROUTING_KEY = "deadQueue";
    /**
     * 死信队列名称
     */
    public final static String DEAD_QUEUE = "deadQueue";

    /**
     * 构建延迟消息交换器
     *
     */
    public static CustomExchange buildDelayExchange() {
        Map<String, Object> args = new HashMap<>(1);
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DEFAULT_DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
    }

    /**
     * 构建普通交换机
     */
    public static DirectExchange buildDirectExchange() {return new DirectExchange(DEFAULT_DIRECT_EXCHANGE, true, false);
    }

    /**
     * 构建死信交换机
     */
    public static DirectExchange buildDeadExchange() {
        return new DirectExchange(DEAD_EXCHANGE, true, false);
    }
}

  2)创建死信队列并绑定到死信交换机

DirectExchange deadExchange = ExchangeBuilder.buildDeadExchange();
rabbitAdmin.declareExchange(exchange);
Queue queue = new Queue(ExchangeBuilder.DEAD_QUEUE);
rabbitAdmin.declareQueue(queue); 
Binding binding = BindingBuilder.bind(queue).to(deadExchange).with(ExchangeBuilder.DEAD_QUEUE);
            rabbitAdmin.declareBinding(binding);

  3)普通队列绑定死信交换机

                    // 队列绑定我们的死信交换机
                    Map<String, Object> arguments = new HashMap<>(2);
                    //死信交换机
                    arguments.put("x-dead-letter-exchange", ExchangeBuilder.DEAD_EXCHANGE);
                    //死信队列
                    arguments.put("x-dead-letter-routing-key", ExchangeBuilder.DEAD_EXCHANGE_ROUTING_KEY);
                    Queue queue = new Queue(queueName, true, false, false, arguments);
                    rabbitAdmin.declareQueue(queue)

   出现如下features表示消息成为死信消息后,会进入死信队列

   4)更改配置

        #重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
        default-requeue-rejected: false

  5)失败重试后进入死信队列有两种方案

    方案一:使用自动ACK + RabbitMQ重试机制

      只需更改配置acknowledge-mode为auto,变为自动ack,达到最大失败次数后会自动进入我们配置的死信队列中

    方案二:使用手动ACK + 手动重试机制

      ①配置acknowledge-mode为manual,手动ack。

      ②代码中控制,达到失败重试次数后,拒绝消息

    private final String REPEAT_CHECK = "rabbitmq:repeatCheck:";

    private final String RETRY_COUNT = "rabbitmq:retryCount:";
    /**
     * 消息最大重试次数
     */
    @Value("${spring.rabbitmq.listener.simple.retry.max-attempts}")
    private int MAX_RETRIES;  



         // 正常消费消息
        try {
            mqListener.handler(t, channel);
            channel.basicAck(deliveryTag, false);
            redisUtil.set(REPEAT_CHECK+version, "1", expireSecond);
        }catch (Exception e){
            // 删除重复消费标记,以免失败重试时,被误认为是重复消息
            if(StrUtil.isNotEmpty(version)){
                redisUtil.del(REPEAT_CHECK + version);
            }

            // 达到失败重试次数后,进入死信队列
            Object retryCountObj = redisUtil.get(RETRY_COUNT + version);
            if(retryCountObj == null){
                redisUtil.set(RETRY_COUNT+version, 1);
            }else {
                int retryCount = Integer.valueOf(retryCountObj.toString()) + 1;
                if(retryCount>=MAX_RETRIES){
                    // 拒绝,不重新入队。则会进入死信队列
                    channel.basicNack(deliveryTag, false, false);
                    redisUtil.del(RETRY_COUNT+version);
                }else {
                    redisUtil.set(RETRY_COUNT+version, retryCount);
                }
            }

            throw e;
        }    

  总结:两种方案都可以达到我们的预期效果,相比起来方案一会更加的方便简洁,方案二的可控性更高

 三、持久化数据库+人工处理

死信队列消费端将所有消息持久化到数据库,人工根据具体情况写定时任务或其他方式处理

 

posted @ 2023-05-08 10:15  joel1889  阅读(3112)  评论(0编辑  收藏  举报