默认会重试所有异常,包括不应重试的业务异常;应禁用自动重试,改用 channel.basicReject(tag, false) 立即拒收且不入队,避免无效重试。

Spring Boot 中 @RabbitListener 遇到业务异常默认会重试,这不是你想要的
Spring AMQP 默认开启消息重试(通过 SimpleRabbitListenerContainerFactory 的 setAdviceChain 或内置的 RetryOperationsInterceptor),哪怕抛的是 IllegalArgumentException 这类明确不该重试的业务异常,也会被当作失败进重试队列,甚至最终进死信。关键在于:Spring 不区分“可恢复异常”和“业务拒绝异常”,它只看是否抛出异常。
用 ImmediateRequeueRejectPolicy 实现「立刻拒收、不入队列、不重试」
这是最轻量且符合语义的做法:让监听器在捕获特定异常时,直接通知 RabbitMQ 拒收当前消息,并要求 Broker **不重新入队**(即不走 basic.reject + requeue=true 那套循环)。需手动配置容器工厂的 defaultRequeueRejected 为 false,并搭配自定义异常处理逻辑:
- 在
@RabbitListener方法内try/catch住你要拦截的业务异常(如InvalidOrderException) - catch 块中显式调用
channel.basicReject(deliveryTag, false)—— 注意第二个参数必须是false - 确保监听器所在容器工厂已设
factory.setDefaultRequeueRejected(false),否则basicReject(..., false)可能被覆盖 - 别依赖
@ExceptionHandler全局捕获——它发生在事务回滚后,此时消息已处于待确认/待拒绝状态,时机不对
RejectAndDontRequeueRecoverer 是个陷阱,别直接用
这个类名字看起来很对口,但它只在 RetryTemplate 抛出最终异常后才触发,也就是说:它前面已经按配置重试了 N 次(比如 3 次),最后才丢弃。如果你的目标是「第一次就拒绝」,它完全不符合需求。常见误用场景是配了 statelessRetry + 这个 recoverer,结果日志里看到重复消费三次再进死信,而不是零次重试。
真正零重试的关键是绕过整个 Retry 流程
最稳妥的方式是彻底禁用监听器级别的自动重试,把控制权交还给业务代码:
- 在容器工厂配置中移除或禁用
RetryOperationsInterceptor,例如:不调用factory.setAdviceChain(...),或显式设factory.setAdviceChain()为空数组 - 监听方法自己处理所有异常,对非业务异常(如网络抖动、DB 超时)可选择记录并抛出,留给基础设施层兜底;对业务异常(如
MissingUserIdException)则立即channel.basicReject(tag, false) - 注意
channel和deliveryTag需通过方法参数注入:@RabbitListener方法签名里加Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag - 拒绝后记得
channel.waitForConfirms()(可选,用于同步确认 Broker 已接收 reject 指令,避免连接中断导致消息悬空)
业务异常的本质是「这条消息永远不该被成功处理」,不是「现在不能处理,等会儿可以」。绕过重试框架、亲手调用 basicReject 才是最贴近 RabbitMQ 原语的做法。很多人卡在想用注解或配置一劳永逸,但 RabbitMQ 的拒绝语义必须由消费者主动发起,框架不会替你做这个判断。










