05-RabbitMQ消息可靠性问题解决
1.消息丢失
消息从生产者到exchange再到queue,再到消费者,有哪些丢失消息的可能性?
(1)发送时丢失:
生产者未发送到exchange丢失;
生产者发送到exchange未发送到queue丢失;
(2)MQ宕机,queue消息丢失
(3)consumer接收到消息后未消费就宕机
2.消息丢失解决方案
(1)生产者消息确认机制;
(2)消息持久化
(3)消费者确认机制
(4)消费失败重试机制
2.1生产者消息确认机制
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种请求:
&publisher-confirm,发送者确认:
消息成功投递到交换机,返回ack。
消息未投递到交换机,返回nack。
&publisher-return,发送者回执:
消息投递到交换机了,但是没有路由到队列,返回ack,及路由失败原因。
注意:确认机制发送消息时,需要给消息设置一个全局的唯一id,区分不同的消息,避免ack冲突。
2.2springAMQP实现生产者确认
(1)在生产者spplication.yml中添加配置:
spring: rabbitmq: publisher-confirm-type: correlated publisher-returns: true template: mandatory: true
配置说明:
&publisher-confirm-type,开启publisher-confirm,这里支持两种类型:
simple:同步等待confirm结果,直到超时
correlated:异步回调,定义confirmCallback,MQ返回结果是会调用confirmCallback方法。
&publisher-returns:开启publisher-return功能,同样是基于callback回调机制,只不过回调的是returnCallback。
&template.mandatory:定义消息路由失败时的策略。false,直接丢弃消息,true,回调returnCallback。
(2)配置ReturnCallBack
每个RabbitTemplate只能配置一个returnCallback,因此需要在项目启动时配置:
@Slf4j @Configuration public class CommonConfig implements ApplicationContextAware { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // 获取RabbitTemplate对象 RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); // 配置ReturnCallback rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> { // 记录日志 log.error("消息发送到队列失败,响应码:{}, 失败原因:{}, 交换机: {}, 路由key:{}, 消息: {}", replyCode, replyText, exchange, routingKey, message.toString()); // 如果有需要的话,重发消息 }); } }
(3)publish-confirm消息确认
@Test public void testSendMessage2SimpleQueue() throws InterruptedException { // 1.准备消息 String message = "hello, spring amqp!"; // 2.准备CorrelationData // 2.1.消息ID CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 2.2.准备ConfirmCallback correlationData.getFuture().addCallback(result -> { // 判断结果 if (result.isAck()) { // ACK log.debug("消息成功投递到交换机!消息ID: {}", correlationData.getId()); } else { // NACK log.error("消息投递到交换机失败!消息ID:{}", correlationData.getId()); // 重发消息 } }, ex -> { // 记录日志 log.error("消息发送失败!", ex); // 重发消息 }); // 3.发送消息 rabbitTemplate.convertAndSend("amq.topic", "a.simple.test", message, correlationData); }
总结:
springAMQP处理消息确认的几种情况:
&publisher-confirm:
消息成功发送到exchange,返回ack;
消息发送失败没有发送到exchange,返回nack;
消息发送过程中发送异常,没有回收回执。
&publisher-returns:
消息成功发送到exchange,但没有路由到queue,调用returnCallback;
2.3消息持久化
(1)交换机持久化
@Bean
public DirectExchange simpleExchange(){ //三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除 return new DirectExchange("simple.direct", true, false);}
(2)队列持久化
@Bean
public Queue simpleQueue(){ // 使用QueueBuilder构建队列,durable就是持久化的
return QueueBuilder.durable("simple.queue").build();}
(3)消息持久化
Message msg = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))//消息体 .setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 持久化 .build();
2.4消费者消息确认
RabbitMQ支持消费者确认模式,即消费者处理消息后会向MQ发送ack回执,MQ收到ack回执才会删除消息。springAMQP支持三种消息确认模式:
&manual:手动ack,需要在也去代码结束后,调用api发送ack;
&auto:自动ack,由spring监测RabbitListener,没有异常返回ack,抛出异常返回nack;
&none:关闭ack,MQ假定消费者获取消息后会处理成功,,因此消息投递成功后会立即删除;
spring: rabbitmq: listener: simple: prefetch: 1 acknowledge-mode: auto
2.5失败重试机制
当消费者出现异常时,消息会不断requeue(重新入队)到队列,再重新发送给消费者,再次异常,再次requeue,导致无限循环,导致MQ消息处理飙升,带来不必要的压力。
可以使用spring的retry机制在消费者出现异常时利用本地重试,而不是无限制的重新requeue。
spring: rabbitmq: listener: simple: prefetch: 1 acknowledge-mode: auto retry: enabled: true # 开启消费者失败重试 initial-interval: 1000 # 初始的失败等待时长为1秒 multiplier: 3 # 下次失败的等待时长倍数,下次等待时长 = multiplier * last-interval max-attempts: 4 # 最大重试次数 stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
2.6消费者失败消息处理策略
&RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式;
&ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队;
&RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机;
测试下RepublishMessageRecoverer处理模式:
&首先,定义接收失败消息的交换机、队列及其绑定关系:
@Bean public DirectExchange errorMessageExchange(){ return new DirectExchange("error.direct"); } @Bean public Queue errorQueue(){ return new Queue("error.queue"); } @Bean public Binding errorMessageBinding(){ return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error"); }
&然后,定义RepublishMessageRecoverer:
@Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error"); }