rabbitmq:retry重试机制和延迟消息的实现

文章介绍了如何在RabbitMQ中配置和使用retry机制来处理消费者消息消费失败的情况,包括手动ack机制、重试配置以及在重试失败后的MessageRecoverer策略。此外,还详细讲解了如何设置延迟消息,通过创建死信队列和设置消息TTL来实现消息超时后转为死信的处理流程。这些机制对于确保业务的稳定性和消息的正确处理至关重要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

rabbitmq:retry重试机制和延迟消息的实现

在消费者消费消息的时候可能会因为网络等外部原因导致消息处理失败,这个时候如果将消息直接丢弃会导致正常的业务丢失,但是如果是一条本身就有问题的消息,那么这个时候又必须丢弃掉,如果选择用channel.basicNackchannel.basicReject方法让消息重回对了,会导致消费者在不停的消费这条消息,这将是一个致命的问题。

所幸,rabbitmq提供了retry机制来控制消息的重试

在这里插入图片描述

yml配置文件:

spring:  
  rabbitmq:
    host: IP
    port: 5672
    username: guest
    password: guest
    virtual-host: smallJHost
    # 消费者确认机制相关配置
    # 开启publisher-confirm,
    # 这里支持两种类型:simple:同步等待confirm结果,直到超时;# correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
    publisher-confirm-type: correlated
    # publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
    publisher-returns: true
    # 定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
    template:
      mandatory: true
    listener:
      simple:
        # ack机制类型
        acknowledge-mode: manual
        # 设置预取消息数量
        prefetch: 2
        # 失败重试
        retry:
          # 开启消费者失败重试
          enabled: true
          # 初始的失败等待时长为1秒
          initial-interval: 1000
          # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          multiplier: 3
          # 最大重试次数
          max-attempts: 4
          # true无状态;false有状态。如果业务中包含事务,这里改为false
          stateless: true

在RabbitmqConfig中增加如下配置:

package com.gitee.small.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class RabbitMQConfig implements ApplicationContextAware {

    // 其他队列、交换机、绑定、回调等代码省略,需要的朋友可看我之前的文章、、、
    
    @Bean
    public Queue errorQueue() {
        return new Queue("error");
    }

    @Bean
    public TopicExchange exchange() {
        return new TopicExchange("topicExchange");
    }


    @Bean(name = "binding.error")
    public Binding bindingExchangeMessage3() {
        return BindingBuilder.bind(errorQueue()).to(exchange()).with("error");
    }

    /**
     * 定义 MessageRecoverer 将错误消息发送到指定队列
     */
    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate, "topicExchange", "error");
    }
}

在消费者定义中有一点需要注意,不能直接将异常处理掉,否则是不会将消息发送到error队列的。

package com.gitee.small.rabbitmq;

import com.rabbitmq.client.Channel;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class WorkRabbitReceiver {

    private static Integer index = 0;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "监听队列名称"),
            exchange = @Exchange(value = "binding对象beanname", type = ExchangeTypes.TOPIC))
    )
    public void process(String msg, Channel channel, Message message) throws Exception {
        try {
            System.out.println(1/0);
        } catch (Exception e) {
            log.error("消息重试");
            throw new Exception();
        }
    }
}

小结:

  • 消息重试是在本地进行重试,不会回到消息队列中

  • 重试模式下,重试次数耗尽后,如果消息依然失败,为了防止消息被直接丢弃,需要有MessageRecovery 接口来处理,它包含三种不同的实现

    • RejectAndDontRequeueRecoverer:重试耗尽后,直接 reject,丢弃消息。默认就是这种方式

    • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

    • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

      很显然,RepublishMessageRecoverer方案应用最广最合理,本文中也是以此为例

实现延迟队列

消息超时方案:

  • 给队列设置 ttl 属性,进入队列后超过 ttl 时间的消息变为死信
  • 给消息设置 ttl 属性,队列接收到消息超过 ttl 时间后变为死信

本文讲给消息设置超时,因为这个方案更灵活。

  1. 创建死信队列和死信交换机,并将其绑定

    @Bean
    public DirectExchange dlExchange() {
        // 声明死信交换机 dl.direct
        return new DirectExchange("dl.direct", true, false);
    }
    
    
    @Bean
    public Queue dlQueue() {
        // 声明存储死信的队列 dl.queue
        return new Queue("dl.queue", true);
    }
    
    
    @Bean(name = "binding.dl")
    public Binding dlBinding() {
        // 将死信队列 与 死信交换机绑定
        return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("dl");
    }
    
  2. 指定消息过期时间,向正常消息队列发送消息,一条5秒延时,一条10秒延时

    private void deadLetter() {
        final Message message = MessageBuilder.withBody("延迟消息测试".getBytes(StandardCharsets.UTF_8))
            .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
            .setExpiration("5000")
            .build();
    
        final Message message2 = MessageBuilder.withBody("延迟消息测试".getBytes())
            .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
            .setExpiration("10000")
            .build();
    
        rabbitTemplate.convertAndSend("topicExchange", "topic.dead", message);
        rabbitTemplate.convertAndSend("topicExchange", "topic.dead", message2);
    }
    
  3. 监听死信队列,实现延迟消息具体逻辑

    /**
    * 监听死信队列,处理延迟消息
    */
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "dl.queue"),
        exchange = @Exchange(value = "binding.dl", type = ExchangeTypes.TOPIC))
                   )
    public void process(String msg, Channel channel, Message message) throws IOException {
    
        log.info("延迟消息:{}", msg);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    
    }
    

使用场景:

  • 延迟发送短信
  • 用户下单,如果用户在一小时内未支付,自动取消
  • 会议前半小时提醒参会

小结:

  1. 创建一个交换机作为死信交换机并绑定一个队列作为死信队列
  2. 给消息的目标队列设置队列超时时间并指定死信交换机和路由 key
  3. 将消息的目标队列绑定到死信交换机
  4. 消费者监听死信队列获取超时消息
### RabbitMQ 生产者消息重试机制实现方式与最佳实践 在实际应用中,为了确保消息能够可靠地传递至目标队列并被消费,RabbitMQ 提供了多种机制来支持生产者的消息重试功能。以下是关于如何通过 RabbitMQ 实现生产者端的消息重试以及一些推荐的最佳实践。 #### 使用 ConfirmListener 确认消息发送状态 当生产者向 RabbitMQ Broker 发送消息时,可以通过启用 `ConfirmListener` 来监听消息的状态反馈。Broker 接收到消息后会返回 ACK 或 NACK 响应给生产者。如果接收到的是 ACK,则表示消息已成功投递;如果是 NACK,则表明消息未能到达预期位置,此时可以触发重试逻辑[^3]。 ```java channel.confirmSelect(); // 开启发布确认模式 channel.addConfirmListener(new ConfirmListener() { @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.out.println("Message successfully delivered."); } @Override public void handleNack(long deliveryTag, boolean multiple) throws IOException { retrySendMessage(deliveryTag); // 自定义方法用于重新发送未成功的消息 } }); ``` #### 设置 ReturnCallback 处理不可路由消息 除了确认消息是否送达外,还需要考虑那些无法根据指定 Exchange Routing Key 正确匹配到任何 Queue 的情况。为此,可以在创建 Channel 后设置 ReturnCallback ,这样一旦有此类事件发生即可捕获相应数据以便后续操作,比如再次尝试分发或者存储起来等待人工干预解决等问题。 ```java channel.addReturnListener((replyCode, replyText, exchange, routingKey, properties, body) -> { String message = new String(body); log.error("Unroutable message: {} with props {}", message, properties); resendOrStoreForLaterHandling(message); // 定义函数处理这些退回的消息 }); // 需要在声明exchange之前调用basicPublish前执行下面这句代码开启mandatory标志位 channel.setReturnHandler(true); ``` #### 控制重试策略避免无限循环 为了避免因网络波动等原因造成不必要的资源消耗甚至死锁现象,在设计重试流程的时候应该加入合理的限制条件,例如最大允许尝试次数或是每次间隔时间的增长比例等参数控制整个过程更加稳健高效。 - **指数退避算法(Exponential Backoff)** 是一种常见的做法,它会在每一次新的请求之间增加一定的延迟量级从而减少短期内频繁失败带来的压力影响。 ```python import time def send_message_with_retry(channel, msg, max_retries=10): retries = 0 while retries < max_retries: try: channel.basic_publish(exchange='my_exchange', routing_key='key', body=msg, mandatory=True) break except Exception as e: wait_time = (2 ** retries) * random.uniform(0.5, 1.5) print(f"Failed to publish after {retries} attempts. Retrying in {wait_time}s...") time.sleep(wait_time) retries += 1 else: save_to_compensation_log(msg) # 如果达到上限仍不成功则记录下来交给补偿服务处理 ``` #### 数据持久化保障最终一致性 即使采取上述措施也不能完全杜绝极端状况下的丢失风险,因此建议将所有待发送的数据先保存于本地数据库或其他可靠的外部媒介之中直到得到明确的成功响应为止 。如此一来即便遇到突发事故也能依靠事后恢复手段维持系统的整体稳定运行。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值