1.3.消费者消息确认
RabbitMQ是阅后即焚机制,RabbitMQ确认消息被消费者消费后会立刻删除。
而RabbitMQ是通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,应该向RabbitMQ发送ACK回执,表明自己已经处理消息。
设想这样的场景:
- 1)RabbitMQ投递消息给消费者
- 2)消费者获取消息后,返回ACK给RabbitMQ
- 3)RabbitMQ删除消息
- 4)消费者宕机,消息尚未处理
这样,消息就丢失了。因此消费者返回ACK的时机非常重要。
而SpringAMQP则允许配置三种确认模式:
•manual:手动ack,需要在业务代码结束后,调用api发送ack。
•auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
•none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
由此可知:
- none模式下,消息投递是不可靠的,可能丢失
- auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
- manual:自己根据业务情况,判断什么时候该ack
一般,我们都是使用默认的auto即可。
1.3.1.演示none模式
修改consumer服务的application.yml文件,添加下面内容:
1. spring: 2. rabbitmq: 3. listener: 4. simple: 5. acknowledge-mode: none # 关闭ack
修改consumer服务的SpringRabbitListener类中的方法,模拟一个消息处理异常:
1. @RabbitListener(queues = "simple.queue") 2. public void listenSimpleQueue(String msg) { 3. log.info("消费者接收到simple.queue的消息:【{}】", msg); 4. // 模拟异常 5. System.out.println(1 / 0); 6. log.debug("消息处理完成!"); 7. }
测试可以发现,当消息处理抛异常时,消息依然被RabbitMQ删除了。
1.3.2.演示auto模式
再次把确认机制修改为auto:
1. spring: 2. rabbitmq: 3. listener: 4. simple: 5. acknowledge-mode: auto # 关闭ack
在异常位置打断点,再次发送消息,程序卡在断点时,可以发现此时消息状态为unack(未确定状态):
抛出异常后,因为Spring会自动返回nack,所以消息恢复至Ready状态,并且没有被RabbitMQ删除:
1.4.消费失败重试机制
当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力:
怎么办呢?
1.4.1.本地重试
我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
修改consumer服务的application.yml文件,添加内容:
1. spring: 2. rabbitmq: 3. listener: 4. simple: 5. retry: 6. enabled: true # 开启消费者失败重试 7. initial-interval: 1000 # 初识的失败等待时长为1秒 8. multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval 9. max-attempts: 3 # 最大重试次数 10. stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
重启consumer服务,重复之前的测试。可以发现:
- 在重试3次后,SpringAMQP会抛出异常AmqpRejectAndDontRequeueException,说明本地重试触发了
- 查看RabbitMQ控制台,发现消息被删除了,说明最后SpringAMQP返回的是ack,mq删除消息了
结论:
- 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
- 重试达到最大次数后,Spring会返回ack,消息会被丢弃
1.4.2.失败策略
在之前的测试中,达到最大重试次数后,消息会被丢弃,这是由Spring内部机制决定的。
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:
- RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。
1)在consumer服务中定义处理失败消息的交换机和队列
1. @Bean 2. public DirectExchange errorMessageExchange(){ 3. return new DirectExchange("error.direct"); 4. } 5. @Bean 6. public Queue errorQueue(){ 7. return new Queue("error.queue", true); 8. } 9. @Bean 10. public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){ 11. return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error"); 12. }
2)定义一个RepublishMessageRecoverer,关联队列和交换机
1. @Bean 2. public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ 3. return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error"); 4. }
完整代码:
1. package cn.itcast.mq.config; 2. 3. import org.springframework.amqp.core.Binding; 4. import org.springframework.amqp.core.BindingBuilder; 5. import org.springframework.amqp.core.DirectExchange; 6. import org.springframework.amqp.core.Queue; 7. import org.springframework.amqp.rabbit.core.RabbitTemplate; 8. import org.springframework.amqp.rabbit.retry.MessageRecoverer; 9. import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; 10. import org.springframework.context.annotation.Bean; 11. 12. @Configuration 13. public class ErrorMessageConfig { 14. @Bean 15. public DirectExchange errorMessageExchange(){ 16. return new DirectExchange("error.direct"); 17. } 18. @Bean 19. public Queue errorQueue(){ 20. return new Queue("error.queue", true); 21. } 22. @Bean 23. public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){ 24. return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error"); 25. } 26. 27. @Bean 28. public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ 29. return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error"); 30. } 31. }
1.5.总结
如何确保RabbitMQ消息的可靠性?
- 开启生产者确认机制,确保生产者的消息能到达队列
- 开启持久化功能,确保消息未消费前在队列中不会丢失
- 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
- 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理