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