RabbitMQ--消费端异常处理与 Spring Retry

1. 消息确认机制(ack)

RabbitMQ 消息投递到消费者后,必须确认(ack)才能从队列中移除:

  • auto-ack = true

    • 消息一投递就算消费成功。

    • 如果消费者宕机,消息会丢失。

    • 一般不用。

  • manual-ack = false(默认)

    • 由 Spring AMQP 或手动调用 basicAck 来确认。

    • 消费成功 → basicAck

    • 消费失败 → basicNackbasicReject

    • 是否重回队列取决于 requeue 参数。


2. Spring Retry 机制

捕获位置

  • Spring Retry 通过 AOP 代理在方法外部包裹一个"重试拦截器"

  • 异常必须从方法栈顶抛出到代理外层才能被捕获

  • 方法内部 try-catch 捕获的异常 不会冒泡到代理外层 → Retry 无法捕获

如何才能触发

  • 必须在 @RabbitListener 方法上出现异常并且不处理,Spring Retry 才能捕获并重试

  • 方法内部捕获异常或自己处理掉 → Retry 无法触发

Spring Boot 已内置 RetryTemplate,只要配置就能在消费者异常时自动重试。

配置示例(application.yml)

acknowledge-mode: auto --》消息会在消费者方法执行完毕后被自动确认(ACK)

java 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto# 一般用自动ack 消息会在消费者方法执行完毕后被自动确认(ACK)
        retry:
          enabled: true            # 开启消费者重试
          max-attempts: 5          # 最大重试次数
          initial-interval: 1000   # 第一次重试间隔 1s
          multiplier: 2.0          # 重试间隔倍数(指数退避)
          max-interval: 10000      # 最大重试间隔 10s

消费者示例

java 复制代码
@Component
public class RetryConsumer {

    @RabbitListener(queues = "test.retry.queue")
    public void onMessage(String msg) {
        System.out.println("收到消息:" + msg);

        // 模拟业务异常
        if (msg.contains("error")) {
            throw new RuntimeException("消费失败,触发Spring Retry");
        }

        System.out.println("消费成功:" + msg);
    }
}

执行流程

  1. 第一次失败 → 等待 1s 后再次执行。

  2. 第二次失败 → 等待 2s 后再次执行。

  3. 第三次失败 → 等待 4s 后再次执行。

  4. ...直到 max-attempts 用完。

  5. 超过最大次数 → 调用 RecoveryCallback(默认是丢弃或进入 DLQ)。

👉 注意:Spring Retry 只在 消费者方法抛异常 时才会触发。如果内部用try-catch处理了没有抛出则不会触发Spring Retry

👉 这里也可以把重试几次看做重复消费几次,以及重试的话也会多次执行相同的业务代码


3. 手动 Nack + DLQ(推荐生产场景)

有时我们不想依赖 Spring Retry,而是用 手动 nack 配合 死信队列(DLQ) 遇到异常如何处理。

配置队列(带 DLQ)

java 复制代码
@Configuration
public class RabbitConfig {

    @Bean
    public Queue businessQueue() {
        return QueueBuilder.durable("test.dlx.queue")
                .withArgument("x-dead-letter-exchange", "dlx.exchange") // 绑定死信交换机
                .withArgument("x-dead-letter-routing-key", "dlx.key")
                .build();
    }

    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange("dlx.exchange");
    }

    @Bean
    public Queue deadLetterQueue() {
        return new Queue("dlx.queue");
    }

    @Bean
    public Binding bindingDLQ() {
        return BindingBuilder.bind(deadLetterQueue())
                .to(dlxExchange())
                .with("dlx.key");
    }
}

消费者(手动控制 ack/nack)

java 复制代码
@Component
public class DLQConsumer {

    @RabbitListener(queues = "test.dlx.queue")
    public void onMessage(String msg, Channel channel, Message message) throws IOException {
        try {
            System.out.println("收到消息:" + msg);

            if (msg.contains("error")) {
                throw new RuntimeException("消费失败,进入DLQ");
            }

            // 成功手动确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

        } catch (Exception e) {
            System.err.println("消费异常:" + e.getMessage());

            // 失败:不重回队列,直接进入 DLQ
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

这里就算在yml中定义重试也没有作用,原因如下:

  1. Spring Retry 的工作时机

    • 当你配置了 retry: enabled: true 时,Spring会创建一个代理(AOP Around Advice)来包裹你的 @RabbitListener 方法。

    • 这个代理的逻辑是:当你的监听方法抛出异常,它才会捕获这个异常,并根据配置进行重试(等待间隔、重试次数等)。

    • 在所有重试次数用尽后,如果仍然失败,这个代理会抛出一个 AmqpRejectAndDontRequeueException 异常,这会触发RabbitMQ将消息拒绝并送入死信队列(DLQ)。

  2. 你的代码做了什么

    • 你在方法内部使用了 try-catch捕获了所有异常(Exception e

    • catch 块中,你直接调用了 channel.basicNack(...) 手动拒绝了消息。

    • 关键点 :由于异常被你亲手捕获并处理了,它并没有被抛出到方法之外。因此,外层的Spring Retry代理根本看不到任何异常 ,它认为本次消费已经"成功"处理完毕(尽管是手动Nack了),所以重试机制完全没有机会触发。


4. 对比总结

方案 原理 配置复杂度 重试策略 消息去向 适用场景
Spring Retry Spring AMQP 捕获异常,内部调度重试 简单(yml 配置即可) 指数退避/固定间隔 超过次数 → 默认丢弃或进入 DLQ 开发测试、简单重试需求
手动 Nack + DLQ 消费失败 → basicNack(requeue=false) → 死信队列 → 再投递 较复杂(需要DLQ配置) 由 TTL + DLQ 控制(灵活) 失败消息进入 DLQ,便于监控和人工处理 生产环境,严格保证消息不丢失

5. 推荐做法(生产级)

  1. 不要依赖 auto-ack ,统一用 manual-ack

  2. 开发阶段 → 可以用 Spring Retry 简单实现。

  3. 生产环境 → 建议用 DLQ + TTL 延时重试,可控性强,防止消息丢失。

  4. 关键业务 → 搭配消息追踪 & 异常告警。

相关推荐
shuair6 小时前
07 - spring security基于数据库的账号密码
spring·spring security
Java水解6 小时前
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
后端·spring
杨杨杨大侠7 小时前
第3篇:配置管理的艺术 - 让框架更灵活
java·spring·log4j
Java码农田8 小时前
springmvc源码分析全体流程图
spring·源码
做一位快乐的码农10 小时前
房屋装修设计管理系统的设计与实现/房屋装修管理系统
java·struts·spring·eclipse·tomcat·maven
麦兜*11 小时前
【Prometheus】 + Grafana构建【Redis】智能监控告警体系
java·spring boot·redis·spring·spring cloud·grafana·prometheus
孟婆来包棒棒糖~17 小时前
Maven快速入门
java·spring boot·spring·maven·intellij-idea
于冬恋1 天前
RabbitMQ高级
服务器·网络·rabbitmq
知其然亦知其所以然1 天前
SpringAI:Mistral AI 聊天?一文带你跑通!
后端·spring·openai