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. 关键业务 → 搭配消息追踪 & 异常告警。

相关推荐
小江的记录本11 小时前
【Java基础】泛型:泛型擦除、通配符、上下界限定(附《思维导图》+《面试高频考点清单》)
java·数据结构·后端·mysql·spring·面试·职场和发展
Don.TIk13 小时前
ChapterOne-搭建项目骨架
java·spring·spring cloud·mybatis
南极企鹅13 小时前
事务&@Transactional注解
java·数据库·spring·oracle·mybatis
me83215 小时前
【AI】踩坑LangChain4j集成千问模型:版本适配问题完整解决历程
java·spring·阿里云·ai
吴声子夜歌16 小时前
状态机——Spring State Machine
java·后端·spring
星秀日19 小时前
Spring Boot + Sa-Token 实时聊天系统:用户注册流程源码深度剖析
java·人工智能·spring·状态模式
夜白宋21 小时前
【Spring深入】一、事务
spring
Mahir0821 小时前
Spring 事务深度解析:核心原理与 12 种事务失效场景全解
java·spring·面试·事务失效
摇滚侠21 小时前
SpringCloud 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·spring·spring cloud
敖正炀1 天前
Spring 设计哲学再探:约定优于配置、误用与反模式
spring boot·spring