RabbitMQ--消费端异常处理与 Spring Retry
1. 消息确认机制(ack)
RabbitMQ 消息投递到消费者后,必须确认(ack)才能从队列中移除:
auto-ack = true
消息一投递就算消费成功。
如果消费者宕机,消息会丢失。
一般不用。
manual-ack = false(默认)
由 Spring AMQP 或手动调用
basicAck
来确认。消费成功 →
basicAck
消费失败 →
basicNack
或basicReject
是否重回队列取决于
requeue
参数。
2. Spring Retry 机制
捕获位置
Spring Retry 通过 AOP 代理在方法外部包裹一个“重试拦截器”
异常必须从方法栈顶抛出到代理外层才能被捕获
方法内部 try-catch 捕获的异常 不会冒泡到代理外层 → Retry 无法捕获
如何才能触发
必须在
@RabbitListener
方法上出现异常并且不处理,Spring Retry 才能捕获并重试方法内部捕获异常或自己处理掉 → Retry 无法触发
Spring Boot 已内置 RetryTemplate,只要配置就能在消费者异常时自动重试。
配置示例(application.yml)
acknowledge-mode: auto --》消息会在消费者方法执行完毕后被自动确认(ACK)
spring:rabbitmq:listener:simple:acknowledge-mode: auto# 一般用自动ack 消息会在消费者方法执行完毕后被自动确认(ACK)retry:enabled: true # 开启消费者重试max-attempts: 5 # 最大重试次数initial-interval: 1000 # 第一次重试间隔 1smultiplier: 2.0 # 重试间隔倍数(指数退避)max-interval: 10000 # 最大重试间隔 10s
消费者示例
@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);} }
执行流程
第一次失败 → 等待
1s
后再次执行。第二次失败 → 等待
2s
后再次执行。第三次失败 → 等待
4s
后再次执行。…直到
max-attempts
用完。超过最大次数 → 调用 RecoveryCallback(默认是丢弃或进入 DLQ)。
👉 注意:Spring Retry 只在 消费者方法抛异常 时才会触发。如果内部用try-catch处理了没有抛出则不会触发Spring Retry
👉 这里也可以把重试几次看做重复消费几次,以及重试的话也会多次执行相同的业务代码。
3. 手动 Nack + DLQ(推荐生产场景)
有时我们不想依赖 Spring Retry,而是用 手动 nack 配合 死信队列(DLQ) 遇到异常如何处理。
配置队列(带 DLQ)
@Configuration public class RabbitConfig {@Beanpublic Queue businessQueue() {return QueueBuilder.durable("test.dlx.queue").withArgument("x-dead-letter-exchange", "dlx.exchange") // 绑定死信交换机.withArgument("x-dead-letter-routing-key", "dlx.key").build();}@Beanpublic DirectExchange dlxExchange() {return new DirectExchange("dlx.exchange");}@Beanpublic Queue deadLetterQueue() {return new Queue("dlx.queue");}@Beanpublic Binding bindingDLQ() {return BindingBuilder.bind(deadLetterQueue()).to(dlxExchange()).with("dlx.key");} }
消费者(手动控制 ack/nack)
@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());// 失败:不重回队列,直接进入 DLQchannel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);}} }
这里就算在yml中定义重试也没有作用,原因如下:
Spring Retry 的工作时机:
当你配置了
retry: enabled: true
时,Spring会创建一个代理(AOP Around Advice)来包裹你的@RabbitListener
方法。这个代理的逻辑是:当你的监听方法
抛出异常
时,它才会捕获这个异常,并根据配置进行重试(等待间隔、重试次数等)。在所有重试次数用尽后,如果仍然失败,这个代理会抛出一个
AmqpRejectAndDontRequeueException
异常,这会触发RabbitMQ将消息拒绝并送入死信队列(DLQ)。你的代码做了什么:
你在方法内部使用了
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. 推荐做法(生产级)
不要依赖 auto-ack,统一用 manual-ack。
开发阶段 → 可以用 Spring Retry 简单实现。
生产环境 → 建议用 DLQ + TTL 延时重试,可控性强,防止消息丢失。
关键业务 → 搭配消息追踪 & 异常告警。