问题场景:
RabbitMQ在实际应用中一般会建立为集群来保证消息的不丢失,但是消息的保证传输还是会存在问题,消息的传送主要有一下四部分,生产者,交换机,队列,消费者,主要产生问题的就是交换机和队列,如果在消息发送之后由于各种原因(交换机发生错误,队列发生错误,或者路由就没有正确匹配等等)并没有成功到达消费者,这时候消息已经离开了生产者,就会丢失,这种情况在实际业务中是不可以的,实际生产中一定要保证消息不会丢失,发生错误的消息要进行捕获,并重新由消费者进行发送。
解法1:(发布确认+消息回退)
如上图所示:消息队列为了保证消息的不丢失,通过发布确认机制来保证,具体过程如下:
1、消息从生产者发出之后并不会进行删除,而是暂时保留消息
2、消息来到交换机,如果交换机发生错误没有接收到消息,就会发出错误回调,在回调函数中进行捕捉,将消息重新给到生产者发送。如果交换机成功接收到了消息,就转发给绑定的队列。
3、消息来到队列,如果队列发生错误,没有接收到交换机发送的消息,同样会有错误回调,回调中进行捕获消息,重新给生产者发送,如果成功接收到消息,那么就会由监听该队列的消费者进行消费,消费完成后同样会告诉生产者,消息成功接收,生产者此时才会删除消息,消息发送周期结束。
下面我们通过一个Demo来验证一下发布确认和消息回退的具体实现过程:
基于Springboot环境进行实现
首先我们先建立基本的配置类、生产者类、消费者类
配置类:
@Configuration
public class RabbitMQCallbackConfig {
//队列
private static final String CALLBACKQUEUE = "callbackqueue";
//交换机
private static final String CALLBACKEXCHANGE = "callbackExchange";
@Bean
public Queue callbackqueue(){
return new Queue(CALLBACKQUEUE);
}
@Bean
public TopicExchange callexchange(){
return new TopicExchange(CALLBACKEXCHANGE);
}
@Bean
public Binding callbackbinding(){
return BindingBuilder.bind(callbackqueue()).to(callexchange()).with("callback");
}
}
生产者类:
/**
* 发布确认的实验
* 消息生产者
*/
@Service
@Slf4j
public class CallbackSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendmsg(String message){
log.info("当前时间:{},发送给队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("callbackExchange"+"123","callback",message);
}
}
消费者类:
@Service
@Slf4j
public class CallbackReceiver {
@RabbitListener(queues = "callbackqueue")
public void receivemsg(Message messages){
String msg = new String(messages.getBody());
log.info("当前时间:{},接收消息:{}",new Date().toString(),msg);
}
}
Controller中调用消息生产者:
//发布确认的测试
@GetMapping("/callback/{message}")
public void callbackmessage(@PathVariable String message){
callbackSender.sendmsg(message);
callbackSender.sendmsg2(message);
}
此时,可以完成基本的消息发送,接下来我们在此基础上开始实现发布确认和消息回退。
实现发布确认和消息回退
一、首先我们需要新建一个类来实现RabbitMq的两个分别用来捕捉交换机错误和队列错误时的消息,如下所示:
(1)、交换机确认接口:RabbitTemplate.ConfirmCallback
实现此接口一定要实现confirm函数
confirm包含了3个参数(CorrelationData correlationData, boolean ack, String cause)
* 一个是correlationData 存储消息的ID和自己存储的关于该条消息的信息
* 一个是boolean ack 判断是否发送成功
* 一个是String cause 发送失败是失败的原因
(2)、队列消息回退接口:RabbitTemplate.ReturnsCallback
实现此接口一定要实现returnedMessage函数
returnedMessage旧版本的接口包含了较多的参数,现在包含一个参数returnedMessage
实现的这个接口
查一下PostConstruct注解的一个执行顺序:
解释:我们实现的接口需要注入到原类方法中才可以在调用过程中进入到为我们重新实现的方法来捕捉。同时我们需要在yml文件中进行配置才可。
yml文件中的配置:(相当于开启RabbitMQ在发布模式和消息回退的相关配置)
#设置交换机确认发布模式,默认为禁用
publisher-confirm-type: correlated
#退回消息
publisher-returns: true
接口类的实现:
public class MQCallback implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入接口实现
rabbitTemplate.setConfirmCallback(this);
//注入接口实现
rabbitTemplate.setReturnsCallback(this);
}
/**
* 【重要】实现 ConfirmCallback 是判断消息是否发送到交换机的回调 接口会默认实现confirm方法
* 共有3个参数
* 一个是correlationData 存储消息的ID和自己存储的关于该条消息的信息
* 一个是boolean ack 判断是否发送成功
* 一个是String cause 发送失败是失败的原因
*
* 交换机确认回调方法
* 1、发消息 交换机接收到了 回调
* 1.1 correlationData 保存回调消息的ID及相关消息
* 1.2 交换机收到消息 ack = true
* 1.3 cause null
* 2、发消息 交换机接收失败 回调
* 2.1 correlationData 保存回调消息的ID及相关消息
* 2.2 交换机收到消息 ack = false
* 2.3 cause失败原因
*/
/**交换机发布确认的接口实现的方法
*
* @param correlationData
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String ID = correlationData!=null? correlationData.getId() : " ";
if(ack){
log.info("交换机已经收到了消息,ID:{}",ID);
}else {
log.info("交换机未收到ID:{}的消息,由于{}的原因",ID,cause);
}
}
/**
* 回退接口实现的方法
* 消息传递过程中不可达目的地消息返回给生产者
* 只有不达目的才会进行回退
* @param returnedMessage
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("回退消息{}",returnedMessage);
}
}
二、修改生产者类
生产者类:
/**
* 发布确认的实验
* 消息生产者
*/
@Service
@Slf4j
public class CallbackSender {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 第一种情况:交换机错误,发送失败,被ConfirmCallback接口实现的confirm方法捕获
*/
CorrelationData correlationData = new CorrelationData("1");
public void sendmsg(String message){
log.info("当前时间:{},发送给队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("callbackExchange"+"123","callback",message,correlationData);
}
/**
* 第二种情况:队列发生错误,消息发送到了交换机,但没有到队列,发送失败,被ReturnsCallback接口实现的returnedMessage方法捕获
*/
CorrelationData correlationData2 = new CorrelationData("2");
public void sendmsg2(String message){
log.info("当前时间:{},发送给队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("callbackExchange","callback"+"123",message,correlationData2);
}
}
到此,我们就可以实现这个小的demo了,我们的这个demo是用来验证我们可以捕捉到从交换机丢失的消息和从队列丢失的消息,我们在上面的消费者类中模拟了交换机和队列丢失的两种情况。
模拟交换机接收错误是模拟生产者发送消息时交换机名错误
模拟队列接收错误是通过模拟routingkey发送错误
解法2:(备份交换机)
当发送的消息在发送给队列时丢失时,【当发送给交换机出现问题时此方法不通,因为此方法实现的原理是将原交换机与备用交换机进行了关联,如果消息压根到不了原交换机,那同样到不了备用交换机】不是必须要回退消息来处理这个问题,也可以通过备份交换机来处理,无需回退给生产者重新发送来处理,具体处理流程如下图所示:
具体实现流程:
1、修改配置类:(添加如下内容)
//备用交换机
private static final String BACKUPEXCHANGE = "backexchange";
//备份队列
private static final String BACKQUEUE = "backqueue";
//警告队列
private static final String WARNQUEUE = "warnqueue";
//备份交换机
@Bean
public Queue backqueue(){
return new Queue(BACKQUEUE);
}
@Bean
public Queue warnqueue(){
return new Queue(WARNQUEUE);
}
@Bean
public FanoutExchange backexchange(){
return new FanoutExchange(BACKUPEXCHANGE);
}
@Bean
public Binding backupbinding(){
return BindingBuilder.bind(backqueue()).to(backexchange());
}
@Bean
public Binding warnbinding(){
return BindingBuilder.bind(warnqueue()).to(backexchange());
}
修改原交换机的生成方式:
@Bean
public TopicExchange callexchange(){
//备份交换机改造
//将确认交换机指向本分交换机
return ExchangeBuilder.topicExchange(CALLBACKEXCHANGE).durable(true).withArgument("alternate-exchange",BACKUPEXCHANGE).build();
//原方式
//return new TopicExchange(CALLBACKEXCHANGE);
}
2、新增警告消费者(备份消费者)
@Slf4j
@Component
public class BackWarnReceiver {
@RabbitListener(queues = "warnqueue")
public void receivemsg(Message messages){
String msg = new String(messages.getBody());
log.info("当前时间:{},接收报警消息消息:{}",new Date().toString(),msg);
}
}
到此完成了消息回退改造成备份交换机的代码改造,我们来测试一下~
消息发送错误模拟类型:成功到达交换机,发送到队列时错误
CorrelationData correlationData2 = new CorrelationData("2");
public void sendmsg2(String message){
log.info("当前时间:{},发送给队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("callbackExchange","callback"+"123",message,correlationData2);
}
测试结果:
可以看到报警消费者收到了消息,第三行的输出验证了首先消息到达了交换机,然后由于为到达队列而发送到了备份交换机,然后由警告消费者消费。
思考:此时我们的回退消息的接口方法仍然还在,为什么没有捕捉到呢?
答案:那说明回退消息和备份交换机应该是有优先级的关系的,也就是说备份交换机的机制是高于消息回退机制的。
总结:到此我们将消息的丢失处理方法全部总结完毕,回想一下我们的处理流程是怎么样的呢?
1、通过发布确认来保证消息不会在没有到达消费者前就已经删除,我们通过修改rabbitmq的配置来开启了发布确认和消息回退的配置。
2、发布确认主要是实现了RabbitTemplate.ConfirmCallback接口,重写了方法并注入,保证了跟踪消息是否发送到交换机的这个步骤,对这部分失败的消息进行捕获。
3、消息回退主要是实现了RabbitTemplate.ReturnsCallback接口,重写了方法并注入,保证了跟踪消息是否由交换机发送到队列这个步骤,对这部分发送失败的消息进行捕获。
4、通过备份交换机的操作,优化了消息回退的机制,并且优先级高于消息回退机制,通过给原交换机关联一个备份交换机用来处理交换机发送给队列时失败的消息。
版权归原作者 XZB119211 所有, 如有侵权,请联系我们删除。