当RabbitMQ宕机后, 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。这是比较麻烦,所以该怎样才能使 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?
在之前都没有考虑MQ宕机的情况,如果消息发送了,MQ宕机了,那么消息就会丢失,这里为了解决这个问题,所以消息在发送的时候都进行一个备份,然后发送给MQ,只有MQ收到消息确认之后才能将缓存中备份的数据进行删除,有效的防止消息丢失;
发布确认是发消息到MQ的保障,手动应答是防止消费者嘎了,MQ到消费者消息不丢失的保障
代码架构图
配置文件修改
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
NONE
禁用发布确认模式,是默认值
CORRELATED
发布消息成功到交换器后会触发回调方法
SIMPLE
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,
其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
配置类
package com.feng.springbootrabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author Feng
* @Date 2022/11/26 15:27
* @Version 1.0
* @Description 发布确认(高级) 配置u类
*/
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String CONFIRM_ROUTING_KEY = "key1";
@Bean("confirmExchange")
public DirectExchange confirmExchange() {
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
@Bean
public Binding queueBindingExchange(@Qualifier("confirmQueue")Queue queue, @Qualifier("confirmExchange") DirectExchange directExchange){
return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KEY);
}
}
交换机回调接口
package com.feng.springbootrabbitmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
* @Author Feng
* @Date 2022/11/26 16:09
* @Version 1.0
* @Description 交换机收到发送者消息得回调类
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
//因为这个回调接口是内部接口,所以实现类得注入这个接口
@Resource
private RabbitTemplate rabbitTemplate;
//construct>@Autowired> @PostConstruct
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* 1.发消息,交换机收到消息 回调
*
* @param correlationData 消息的ID以及消息内容
* @param ack 交换机是否收到消息
* @param cause 收到消息失败得原因,成功为NULL
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//对 correlationData 进行判空
String id = correlationData != null ? correlationData.getId() : "";
//成功收到消息
if (ack) {
log.info("交换机已经收到 id 为:{}的消息", id);
}else {
//没有收到消息
log.info("交换机还未收到 id 为:{}消息,原因:{}", id, cause);
}
}
}
生产者
package com.feng.springbootrabbitmq.controller;
import com.feng.springbootrabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/confirm")
@Slf4j
public class ProducerController {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 消息回调和退回
*
* @param message
*/
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable String message) {
//这个类就是消息的ID以及小心哦内容
CorrelationData correlationData =new CorrelationData("1");
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, ConfirmConfig.CONFIRM_ROUTING_KEY, message,correlationData);
log.info(ConfirmConfig.CONFIRM_ROUTING_KEY + "发送消息内容:{}", message);
}
}
消费者
package com.feng.springbootrabbitmq.consumer;
import com.feng.springbootrabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ConfirmConsumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveMsg(Message message) {
String msg = new String(message.getBody());
log.info("接受到队列 confirm.queue 消息:{}", msg);
}
}
问题
假设发送了两条消息,第一条消息的 RoutingKey 为 “key1”,第二条消息的 RoutingKey 为 “key2”,两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有第二条消息被直接丢弃了。
丢弃的消息交换机是不知道的,需要解决告诉生产者消息传送失败
原因
因为这个回调接口是在交换机收到生产者发送的消息后进行回调的,但是如果队列出现了问题,这个时候消息就会出现丢失,生产者是不知道的,那么就只能靠回退消息进行处理
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者
配置文件
spring.rabbitmq.publisher-returns=true
回退接口
低版本可能没有 RabbitTemplate.ReturnsCallback 请用RabbitTemplate.ReturnCallback
package com.feng.springbootrabbitmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
* @Author Feng
* @Date 2022/11/26 16:09
* @Version 1.0
* @Description 交换机收到发送者消息得回调类
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
//因为这个回调接口是内部接口,所以实现类得注入这个接口
@Resource
private RabbitTemplate rabbitTemplate;
//construct>@Autowired> @PostConstruct
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
/**
* true:交换机无法将消息进行路由时,会将该消息返回给生产者
* false:如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(this);
//RabbitMQ版本低的是 rabbitTemplate.setReturnCallback(myCallBack);
}
/**
* 交换机确认回调方法
* 1.发消息,交换机收到消息 回调
*
* @param correlationData 消息的ID以及消息内容
* @param ack 交换机是否收到消息
* @param cause 收到消息失败得原因,成功为NULL
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//对 correlationData 进行判空
String id = correlationData != null ? correlationData.getId() : "";
//成功收到消息
if (ack) {
log.info("交换机已经收到 id 为:{}的消息", id);
}else {
//没有收到消息
log.info("交换机还未收到 id 为:{}消息,原因:{}", id, cause);
}
}
/**
* 当消息无法路由的时候的回调方法
* 只有消息不可达目的地的时候才触发
* @param returned
*/
@Override
public void returnedMessage(ReturnedMessage returned) {
log.error("消息:{},被交换机 {} 退回,原因:{},路由key:{},code:{}",
new String(returned.getMessage().getBody()),
returned.getExchange(),
returned.getReplyText(),
returned.getRoutingKey(),
returned.getReplyCode());
}
}
再次测试,发送一个正确路由,一个错误理由的两个消息
结果
当消息路由不可路由的时候我们只能回退给生产者进行处理,而生产者最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。也会增加生产者的代码逻辑。
在 RabbitMQ 中,有一种备份交换机的机制存在。当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout
,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
代码架构图
这里修改了原有交换机的配置,所以记得将原有的交换机删掉
package com.feng.springbootrabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author Feng
* @Date 2022/11/26 15:27
* @Version 1.0
* @Description 发布确认(高级) 配置u类
*/
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String CONFIRM_ROUTING_KEY = "key1";
//备份交换机
public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
//备份队列
public static final String BACKUP_QUEUE_NAME = "backup.queue";
//警告队列
public static final String WARNING_QUEUE_NAME = "warning.queue";
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
@Bean
public Binding queueBindingExchange(@Qualifier("confirmQueue")Queue queue, @Qualifier("confirmExchange") DirectExchange directExchange){
return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KEY);
}
//声明确认交换机,以及和备份交换机的联系
@Bean("confirmExchange")
public DirectExchange backupConfirmExchange() {
ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
.durable(true)
//设置该交换机的备份交换机
//或者直接用方法 .alternate("备份交换机名")
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
return exchangeBuilder.build();
}
//************************以下是关于备份的******************************
//声明备份 Exchange
@Bean("backupExchange")
public FanoutExchange backupExchange() {
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
// 声明备份队列
@Bean("backQueue")
public Queue backQueue() {
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
// 声明警告队列
@Bean("warningQueue")
public Queue warningQueue() {
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
// 声明备份队列绑定关系
@Bean
public Binding backupBinding(@Qualifier("backQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange backupExchange) {
return BindingBuilder.bind(queue).to(backupExchange);
}
// 声明报警队列绑定关系
@Bean
public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange backupExchange) {
return BindingBuilder.bind(queue).to(backupExchange);
}
}
package com.feng.springbootrabbitmq.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author Feng
* @Date 2022/11/26 17:35
* @Version 1.0
* @Description 报警队列消费者
*/
@Component
@Slf4j
public class WarningConsumer {
public static final String WARNING_QUEUE_NAME = "warning.queue";
@RabbitListener(queues = WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message) {
String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}", msg);
}
}
测试
http://localhost:8080/confirm/sendMessage/喜喜
思考
mandatory 参数(消息回退给生产者)与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。