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)
javaspring: 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); } }
执行流程
第一次失败 → 等待
1s
后再次执行。第二次失败 → 等待
2s
后再次执行。第三次失败 → 等待
4s
后再次执行。...直到
max-attempts
用完。超过最大次数 → 调用 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中定义重试也没有作用,原因如下:
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 延时重试,可控性强,防止消息丢失。
关键业务 → 搭配消息追踪 & 异常告警。
RabbitMQ--消费端异常处理与 Spring Retry
你我约定有三2025-08-25 15:43
相关推荐
ChinaRainbowSea5 小时前
9. LangChain4j + 整合 Spring Boot虫小宝6 小时前
返利app排行榜的缓存更新策略:基于过期时间与主动更新的混合方案zzywxc7877 小时前
AI工具全景洞察:从智能编码到模型训练的全链路剖析沐浴露z14 小时前
【Java SpringAI智能体开发学习 | 2】SpringAI 实用特性:自定义Advisor,结构化输出,对话记忆持久化,prompt模板,多模态招风的黑耳18 小时前
Java生态圈核心组件深度解析:Spring技术栈与分布式系统实战李游Leo21 小时前
Redis 持久化与高可用实践(RDB / AOF / Sentinel / Cluster 全解析)九术沫1 天前
装饰器模式在Spring中的案例fire-flyer1 天前
响应式客户端 WebClient详解北执南念1 天前
基于 Spring 的策略模式框架,用于根据不同的类的标识获取对应的处理器实例