发布确认高级
概念
在之前的学习中,我们学习到了单个确认、批量确认、异步确认三种确认方式,也通过实操来比较三种确认方式的性能,其中异步确认是性能最好的。
现在我们考虑以下这种情况,在生产者发送消息的过程中,可能会出现交换机宕机、队列宕机、或者两者一同宕机的情况。这时候消息就石沉大海了,在不适用集群的情况下,来看看如何解决这一问题。
想象一下,假设是小黄寄了一个快递出去,在运输过程中出现在某些状况导致我的快递丢失,这时候快递公司会发一条消息告诉我快递丢失,并与我协商处理方案
搭建模拟环境
首先我们根据上图来搭建这个例子
编写配置文件
@Configuration
public class ConfirmQueueConfig {
//声明交换机名称
public static String CONFIRM_EXCHANGE = "confirm_exchange";
//声明队列名称
public static String CONFIRM_QUEUE = "confirm_queue";
//声明routingKey
public static String CONFIRM_ROURING_KEY = "confirm_rouring_key";
//声明交换机
@Bean
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE);
}
//声明队列
@Bean
public Queue confirmQueue(){
return new Queue(CONFIRM_QUEUE);
}
//绑定交换机和队列
@Bean
public Binding confirmQueueBindingExchange(@Qualifier("confirmExchange") DirectExchange confirmExchange,
@Qualifier("confirmQueue") Queue confirmQueue){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROURING_KEY);
}
}
编写生产者
@GetMapping("/sendConfirmMsg/{message}")
public void sendConfirmMsg(@PathVariable String message){
log.info("生产者正在发送消息:{}" , message);
rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE,ConfirmQueueConfig.CONFIRM_ROURING_KEY,message);
}
编写消费者
@Slf4j
@Component
public class ConfirmQueueConsumer {
@RabbitListener(queues = "confirm_queue")
public void receiveMsg(Message message){
log.info("消费者接收到消息:{}", new String(message.getBody()));
}
}
处理交换机宕机问题
配置RabbitMQ
spring:
rabbitmq:
publisher-confirm-type: correlated # 默认是none,禁止使用发布确认模式,simple单个确认模式
编写回调函数
@Slf4j
@Component
public class MyCallback implements RabbitTemplate.ConfirmCallback {
@Autowired
RabbitTemplate rabbitTemplate;
//注入自定义回调函数
@PostConstruct
private void init(){
rabbitTemplate.setConfirmCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//交换机的回调函数
if (ack){
//接收成功
log.info("交换机成功接收到id为{}的消息",correlationData.getId());
}else {
//接受失败
log.error("交换机未接收到id为{}的消息,失败原因:{}",correlationData.getId(),cause);
}
}
}
修改生产者代码
发送消息给一个不存在的虚拟机,看是否能回复生产者
@GetMapping("/sendConfirmMsg/{message}")
public void sendConfirmMsg(@PathVariable String message){
log.info("生产者正在发送消息:{}" , message);
CorrelationData correlationData1 = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE,ConfirmQueueConfig.CONFIRM_ROURING_KEY,message,correlationData1);
CorrelationData correlationData2 = new CorrelationData("2");
//模拟交换机宕机,随便搞一个不存在的交换机
rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE + "2",ConfirmQueueConfig.CONFIRM_ROURING_KEY,message,correlationData1);
}
测试
启动测试,可以看到控制台确实能收到失败的消息,到此我们就解决了交换机宕机的问题
处理队列宕机问题
配置RabbitMQ
spring:
rabbitmq:
publisher-returns: true
编写回调函数
@Slf4j
@Component
public class MyCallback implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
@Autowired
RabbitTemplate rabbitTemplate;
//注入自定义回调函数
@PostConstruct
private void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//交换机的回调函数
if (ack){
//接收成功
log.info("交换机成功接收到id为{}的消息",correlationData.getId());
}else {
//接受失败
log.error("交换机未接收到id为{}的消息,失败原因:{}",correlationData.getId(),cause);
}
}
//当消息不可到达目的地时,回退
@Override
public void returnedMessage(ReturnedMessage returned) {
String msg = new String(returned.getMessage().getBody());
log.error("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}",
msg,returned.getReplyText(),returned.getExchange(),returned.getRoutingKey());
}
}
修改生产者代码
@GetMapping("/sendConfirmMsg/{message}")
public void sendConfirmMsg(@PathVariable String message){
log.info("生产者正在发送消息:{}" , message);
CorrelationData correlationData1 = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE,ConfirmQueueConfig.CONFIRM_ROURING_KEY,message,correlationData1);
CorrelationData correlationData2 = new CorrelationData("2");
//模拟队列宕机,写一个不存在的routigKey
rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE,ConfirmQueueConfig.CONFIRM_ROURING_KEY + "2",message,correlationData2);
}
测试
观察控制台输出,可以发现由于队列找不到的消息返回到了生产者
备份交换机
队列宕机之后,我们也需要通知人员来进行处理,这时候就用到了备份交换机,我们设置当消息到达不了目的地时,转发到备份交换机,通过备份队列来保存数据,通过报警队列告诉报警消费者
修改配置文件
在原先的配置文件上,我们需要新增备份交换机、备份队列、报警队列
@Configuration
public class ConfirmQueueConfig {
//声明交换机名称
public static String CONFIRM_EXCHANGE = "confirm_exchange";
//声明队列名称
public static String CONFIRM_QUEUE = "confirm_queue";
//声明routingKey
public static String CONFIRM_ROURING_KEY = "confirm_rouring_key";
//声明备份交换机名称
public static String ALTERNATE_EXCHANGE = "alternate_exchange";
//声明备份队列名称
public static String ALTERNATE_QUEUE = "alternate_queue";
//声明报警队列名称
public static String WARNING_QUEUE = "warning_queue";
//声明备份交换机
@Bean
public FanoutExchange alternateExchange(){
return new FanoutExchange(ALTERNATE_EXCHANGE);
}
//声明备份队列
@Bean
public Queue alternateQueue(){
return new Queue(ALTERNATE_QUEUE);
}
//声明报警队列
@Bean
public Queue warningQueue(){
return new Queue(WARNING_QUEUE);
}
//绑定备份队列和备份交换机
@Bean
public Binding alternateQueueBindingAlternateExchange(@Qualifier("alternateQueue") Queue alternateQueue,
@Qualifier("alternateExchange") FanoutExchange alternateExchange){
return BindingBuilder.bind(alternateQueue).to(alternateExchange);
}
//绑定报警队列和备份交换机
@Bean
public Binding warningQueueBindingAlternateExchange(@Qualifier("warningQueue") Queue warningQueue,
@Qualifier("alternateExchange") FanoutExchange alternateExchange){
return BindingBuilder.bind(warningQueue).to(alternateExchange);
}
//声明交换机
@Bean
public DirectExchange confirmExchange(){
ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE).withArgument("alternate-exchange", ALTERNATE_EXCHANGE);
return (DirectExchange) exchangeBuilder.build();
}
//声明队列
@Bean
public Queue confirmQueue(){
return new Queue(CONFIRM_QUEUE);
}
//绑定交换机和队列
@Bean
public Binding confirmQueueBindingExchange(@Qualifier("confirmExchange") DirectExchange confirmExchange,
@Qualifier("confirmQueue") Queue confirmQueue){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROURING_KEY);
}
}
新增报警消费者
@Slf4j
@Component
public class WarningQueueConsumer {
@RabbitListener(queues = "warning_queue")
public void receiveMsg(Message message){
log.error("报警发现不可路由消息:{}", new String(message.getBody()));
}
}
测试
通过控制台输出,我们可以看到报警消费者确实接收到了报警消息。
这里需要注意的是:当消息回退和备份交换机同时存在时,备份交换机的优先级高