如何在Spring异步MessageListener用例中发生业务异常时请求RabbitMQ重试

时间:2016-05-02 10:05:55

标签: java spring rabbitmq

我正在运行Spring AMQP消息侦听器。

public class ConsumerService implements MessageListener {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Override
    public void onMessage(Message message) {
        try {
            testService.process(message); //This process method can throw Business Exception
        } catch (BusinessException e) {
           //Here we can just log the exception. How the retry attempt is made?
        } catch (Exception e) {
           //Here we can just log the exception.  How the retry attempt is made?
        }
    }
}

如您所见,在处理过程中可能会出现异常。我想重试因为Catch块中的特定错误。我不能通过onMessage中的异常。 如何告诉RabbitMQ有异常并重试?

1 个答案:

答案 0 :(得分:24)

由于onMessage()不允许抛出已检查的异常,因此可以将异常包装在RuntimeException中并重新抛出。

try {
    testService.process(message);
} catch (BusinessException e) {
    throw new RuntimeException(e);
}

但请注意,这可能会导致邮件无限期地重新传递。这是如何工作的:

RabbitMQ支持拒绝邮件并要求代理重新排队。这显示为here。但RabbitMQ本身并没有重试策略的机制,例如:设置最大重试次数,延迟等

使用Spring AMQP时,“拒绝重新排队”是默认选项。当存在未处理的异常时,Spring的SimpleMessageListenerContainer默认会执行此操作。因此,在您的情况下,您只需要重新抛出捕获的异常。但请注意,如果您无法处理消息并且始终抛出异常,则会无限期地重新传递该消息并导致无限循环。

您可以通过抛出AmqpRejectAndDontRequeueException异常来覆盖每条消息的此行为,在这种情况下,消息不会被重新排队。

您还可以通过设置

完全关闭SimpleMessageListenerContainer的“拒绝重新排队”行为
container.setDefaultRequeueRejected(false) 

当邮件被拒绝且没有重新排队时,如果在RabbitMQ中设置了一条消息,它将丢失或转移到DLQ。

如果您需要具有最大尝试次数,延迟等的重试策略,最简单的方法是设置弹簧“无状态”RetryOperationsInterceptor,它将在线程内进行所有重试(使用Thread.sleep())而不拒绝每次重试时都会显示消息(因此每次重试都不会返回RabbitMQ)。重试耗尽时,默认情况下会记录一条警告并消耗该消息。如果您要发送到DLQ,您需要RepublishMessageRecoverer或自定义MessageRecoverer拒绝邮件而不会重新排队(在后一种情况下,您还应setup使用RabbitMQ DLQ队列)。默认消息恢复器的示例:

container.setAdviceChain(new Advice[] {
        org.springframework.amqp.rabbit.config.RetryInterceptorBuilder
                .stateless()
                .maxAttempts(5)
                .backOffOptions(1000, 2, 5000)
                .build()
});

这显然有一个缺点,即您将在整个重试期间占用线程。您还可以选择使用“有状态”RetryOperationsInterceptor,它会在每次重试时将消息发送回RabbitMQ,但延迟仍将在应用程序中使用Thread.sleep()实现,并设置为有状态拦截器有点复杂。

因此,如果您希望在不占用Thread的情况下重试延迟,则需要在RabbitMQ队列上使用TTL进行更多涉及的自定义解决方案。如果你不想要指数退避(所以延迟不会在每次重试时增加),它会更简单一些。要实现这样的解决方案,您基本上在rabbitMQ上使用参数创建另一个队列:"x-message-ttl": <delay time in milliseconds>"x-dead-letter-exchange":"<name of the original queue>"。然后在主队列上设置"x-dead-letter-exchange":"<name of the queue with the TTL>"。所以现在当你拒绝并且没有重新排队消息时,RabbitMQ会将它重定向到第二个队列。当TTL过期时,它将被重定向到原始队列,从而重新传送到应用程序。所以现在你需要一个重试拦截器,在每次失败后拒绝给RabbitMQ的消息,并跟踪重试次数。为了避免在应用程序中保持状态的需要(因为如果您的应用程序是群集的,您需要复制状态),您可以从RabbitMQ设置的x-death标头计算重试计数。查看有关此标题here的更多信息。因此,在这一点上实现自定义拦截器比使用此行为自定义Spring状态拦截器更容易。

同时检查the section about retries in the Spring AMQP reference