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

相关推荐
云烟成雨TD16 小时前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Java成神之路-16 小时前
SpringMVC 响应实战指南:页面、文本、JSON 返回全流程(Spring系列13)
java·spring·json
砍材农夫17 小时前
spring-ai 第六模型介绍-聊天模型
java·人工智能·spring
云烟成雨TD17 小时前
Spring AI Alibaba 1.x 系列【5】ReactAgent 构建器深度源码解析
java·人工智能·spring
Flittly18 小时前
【SpringAIAlibaba新手村系列】(15)MCP Client 调用本地服务
java·笔记·spring·ai·springboot
Flittly18 小时前
【SpringAIAlibaba新手村系列】(14)MCP 本地服务与工具集成
java·spring boot·笔记·spring·ai
mfxcyh19 小时前
基于xml、注解、JavaConfig实现spring的ioc
xml·java·spring
Flittly19 小时前
【SpringAIAlibaba新手村系列】(13)Tool Calling 函数工具调用技术
java·spring boot·spring·ai
xdscode19 小时前
Spring 依赖注入方式全景解析
java·后端·spring