生产者可靠性实现
前言
在发送消息时我们或许想过消息可能会被丢弃。从根据消息传输流程从前往后,我们从生产者开始。生产者有可能在发送时把消息弄丢。当消息传到MQ时,MQ本身也有可能把消息弄丢。而消息从队列送到消费者手里时,消费者也有可能把传来的消息弄丢。因此保证消息可靠性是很重要的。RabbitMQ也正是基于这三种可能性依次提出了解决办法。
生产者可靠性
-
生产者重连 :连接MQ失败的重试,是线程阻塞式的等待重连。(十分影响性能,程序发布时一定要禁用)
直接在配置中配置:spring: rabbitmq: host: IP地址 port: 5672 virtual-host: 主机名 username: 用户名 password: 密码 connection-timeout: 200ms #设置MQ连接超时的时间 template: retry: enabled: true #开启超时重试机制 initial-interval: 1000ms #失败后的初始等待时间 multiplier: 1 #失败后下次的等待时长倍数,下次等待时长 max-attempts: 2 #最大重试次数
-
生产者确认
开启生产者确认机制后,MQ在成功收到消息后会返回确认消息给生产者四种结果:- 消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功
- 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
- 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功
- 其它情况都会返回NACK,告知投递失败
spring: rabbitmq: host: IP地址 port: 5672 virtual-host: 主机名 username: 用户名 password: 密码 publisher-confirm-type: correlated #开启PublishConfirm确认机制 设置Confirm类型 publisher-returns: true #开启PublishReturn机制
2.1 Publish Return
专门返回路由失败的消息,路由失败的原因基本是由开发者导致的(开发者配置不正确的yml、队列不存在、格式不对等)。既然原因一致,那么失败信息除了ID之外高度一致,因此我们可以统一设置Return回调函数的模版格式以提高性能。
我们可以在RabbitMQ模版被注册到容器后立即设置Return回调函数格式,这样就能够在程序运行前预先设定好Return回调函数的模版。为什么要设置Return为回调函数?因为回调是异步执行的,不会阻塞当前线程。那么应该如何统一处理?
我们最好在RabbitMQ模版被注册到容器后马上设置Return回调函数格式,这样能够在程序运行前就提前设定好Return回调函数的模版。
在SpringBoot容器创建完成时的会扫描实现了ApplicationContextAware 接口的类,并把容器传给类中实现接口的方法。因此我们只需要在这个方法里调用即将Return回调函数统一模版设置即可大功告成。
这样只要消息发送失败就会触发我们重写的Return回调函数package com.example.publisher.config; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.ReturnedMessage; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Configuration; @Configuration @Slf4j public class MqConfirmConfig implements ApplicationContextAware{ @Override public void setApplicationContext(ApplicationContext applicationContext)throws BeansException { RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); //设置ReturnCallback回调函数 rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() { @Override public void returnedMessage(ReturnedMessage r) { log.debug("收到消息的return callback,exchange:{],key:f},msg:{},code:{},text:{}", r.getExchange(),r.getRoutingKey(),r.getMessage(),r.getReplyCode() ,r.getReplyText()); } }); } }
2.2 Publish Confirm
确认类型publisher-confirm-type说明:
none:关闭confirm机制
simple:同步阻塞等待MQ的回执消息
correlated:MQ异步回调方式返回回执消息package com.example.publisher; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.util.concurrent.ListenableFutureCallback; import java.util.UUID; @SpringBootTest @Slf4j class PublisherApplicationTests { @Autowired private RabbitTemplate rabbitTemplate; @Test void contextLoads() throws InterruptedException { //1、创建CorrelationData CorrelationData cd = new CorrelationData(UUID.randomUUID().toString()); //2.添加ConfirmCallback cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() { @Override public void onFailure(Throwable ex) { log.error("消息回调失败", ex); } @Override public void onSuccess(CorrelationData.Confirm result) { log.debug("收到confirm callback回执"); if (result.isAck()) { //消息发送成动 log.debug("消息发送成功,收到ack"); } else { //消总发送失败 log.error("消息发送失败,收到nack,原因:{}", result.getReason()); } } }); rabbitTemplate.convertAndSend("xxx.direct", "xxx", "hello", cd); Thread.sleep(2000); } }
这是我随便给不存在的交换机发的消息,最后能在控制台看到类似这种消息
注意:我设置的日志格式等级为debug 我采用的是Lombok+SpringBoot默认的日志框架
logging: pattern: dateformat: MM-dd HH-mm-ss:SSS level: DEBUG