一.发布确认springboot版本
1.理解概念及原因
- 原因:由于不明原因造成rabbitmq重启或宕机,从而导致消息传输失败,需要手动处理和恢复,为了保证消息可靠传输,即在异常情况下也能由备份消息来保证下一次能传输这次失败了的消息,产生了发布确然机制,异常效果图如下:
- 概念:如上图,在消息发送后,如果在交换机宕机或异常情况下,发送者还无法接收到交换机此时状况,来对消息进行缓存,就会导致消息丢失。而如果能在交换机异常时,对未发送成功的消息进行一个缓存,再通过定时任务对这次消息进行重新发送,就能保证信息的不丢失
2.消息确认代码测试
1.交换机异常
-
配置文件添加
spring.rabbitmq.publisher-confirm-type=correlated
以上属性的三种值及分析:
NONE:禁用发布确认模式,是默认值
CORRELATED:发布消息成功到交换机后会触发回调方法
SIMPLE:效果一和CORRELATED一样;效果二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果判断下一步的逻辑,注意点是waitForConfirmsOrDie方法,如果返回false则会关闭channel,则接下来无法发送消息到broker -
创建配置类
@Configuration
public class ComfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String CONFIRM_ROUTINGKEY = "key1";
@Bean("confirmExchange")
public DirectExchange confirmExchage(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
@Bean
public Binding queueBinding(@Qualifier("confirmExchange") DirectExchange directExchange,
@Qualifier("confirmQueue") Queue confirmQueue){
return BindingBuilder.bind(confirmQueue).to(directExchange).with(CONFIRM_ROUTINGKEY);
}
}
- 创建生产者
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
//发信息
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message ){
CorrelationData correlationData1 = new CorrelationData("1");
rabbitTemplate.convertAndSend(ComfirmConfig.CONFIRM_EXCHANGE_NAME, ComfirmConfig.CONFIRM_ROUTINGKEY, message + "key1",correlationData1);
log.info("发送消息内容:{}",message + "key1");
CorrelationData correlationData2 = new CorrelationData("2");
rabbitTemplate.convertAndSend(ComfirmConfig.CONFIRM_EXCHANGE_NAME, ComfirmConfig.CONFIRM_ROUTINGKEY + "123", message + "key12",correlationData2);
log.info("发送消息内容:{}",message + "key12") ;
}
}
- 创建消费者
//接收消息
@Component
@Slf4j
public class Consumer {
@RabbitListener(queues = ComfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveMessage(Message message){
String msg = new String(message.getBody());
log.info("接收到队列confirm.queue信息:{}",msg);
}
}
- 回调函数:发送消息出现异常时进行触发
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
//@PostConstruct在其他注解执行完之后才执行,即先通过@Component实例化,
//然后@Autowired,最后才是用@PostConstruct
public void init(){
//注入实现类
//由于MyCallBack实现的ConfirmCallback接口是RabbitTemplate的内部类,
//所以MyCallBack不在RabbitTemplate对象中,导致将来RabbitTemplate通过ConfirmCallback
//不能调用MyCallBack
//则需要将MyCallBack这个实现类注入到ConfirmCallback接口中
//为了保证执行顺序才使用了@PostConstruct
rabbitTemplate.setConfirmCallback(this);
}
@Override
/**
* 参数解读
* 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-->失败的原因
*/
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);
}
}
}
2.队列异常
- 说明:在仅开启生产者确认机制情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,即交换机无法发送消息到队列,则消息会被丢弃,此时生产者不知道消息被丢失的这件事
- 解决:设置mandatory参数,当消息传递过程中不可达目的地时将信息返回给生产者
- 配置参数
spring.rabbitmq.publisher-returns=true
- 添加回调函数(类MyCallBack中)
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
//该注解在其他注解执行完之后才执行,即先通过@Component实例化,
//然后@Autowired,最后才是用@PostConstruct将
public void init(){
//注入实现类
//由于MyCallBack实现的ConfirmCallback接口是RabbitTemplate的内部类,
//所以MyCallBack不在RabbitTemplate对象中,导致将来RabbitTemplate通过ConfirmCallback
//不能调用MyCallBack
//则需要将MyCallBack这个实现类注入到ConfirmCallback接口中(对于ReturnsCallback同理)
//为了保证执行顺序才使用了@PostConstruct
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData != null ? correlationData.getId() : "";
if(b){
log.info("交换机已经收到id为:{}的消息", id);
}else{
log.info("交换机还未收到ID为:{},原因为:{}", id, s);
}
}
//可以在当消息传递过程中不可达目的地时才将消息返回给生产者
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("信息{},被交换机{}退回,原因:{},路由key:{}",
new String(returnedMessage.getMessage().getBody()), returnedMessage.getExchange()
,returnedMessage.getReplyText(),returnedMessage.getRoutingKey());
}
}
二.备份交换机
1.基本介绍
- 问题:当生产者所在服务有多台机器时,手动处理无法路由的消息会很麻烦且容易出错。
- 解决方案:使用备份交换机。当交换机接收到一条不可路由的消息时,会把这条消息转发到备份交换机中,由它来进行转发和处理,通常类型为Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后用备份交换机绑定一个队列,这样所有原来交换机无法被路由的消息,就都会进入这个队列
- 代码架构图
2.代码测试
- 配置类
@Configuration
public class ComfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String CONFIRM_ROUTINGKEY = "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("confirmExchange")
public DirectExchange confirmExchage(){
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
.withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
@Bean
public Binding queueBinding(@Qualifier("confirmExchange") DirectExchange directExchange,
@Qualifier("confirmQueue") Queue confirmQueue){
return BindingBuilder.bind(confirmQueue).to(directExchange).with(CONFIRM_ROUTINGKEY);
}
@Bean("backupQueue")
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
@Bean("warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
@Bean
public Binding backupQueueBindBackupExchange(@Qualifier("backupQueue") Queue backupQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
@Bean
public Binding warningQueueBindBackupExchange(@Qualifier("warningQueue") Queue warningQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
- 生产者
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message ){
CorrelationData correlationData1 = new CorrelationData("1");
rabbitTemplate.convertAndSend(ComfirmConfig.CONFIRM_EXCHANGE_NAME, ComfirmConfig.CONFIRM_ROUTINGKEY, message + "key1",correlationData1);
log.info("发送消息内容:{}",message + "key1");
CorrelationData correlationData2 = new CorrelationData("2");
rabbitTemplate.convertAndSend(ComfirmConfig.CONFIRM_EXCHANGE_NAME, ComfirmConfig.CONFIRM_ROUTINGKEY + "123", message + "key12",correlationData2);
log.info("发送消息内容:{}",message + "key12") ;
}
}
- 测试结果
2021-12-03 11:36:36.163 INFO 25340 --- [nio-8080-exec-1] c.e.r.controller.ProducerController : 发送消息内容:发扣扣key1
2021-12-03 11:36:36.167 INFO 25340 --- [nectionFactory1] com.example.rabbitmq.config.MyCallBack : 交换机已经收到id为:1的消息
2021-12-03 11:36:36.167 INFO 25340 --- [nio-8080-exec-1] c.e.r.controller.ProducerController : 发送消息内容:发扣扣key12
2021-12-03 11:36:36.171 INFO 25340 --- [nectionFactory1] com.example.rabbitmq.config.MyCallBack : 交换机已经收到id为:2的消息
2021-12-03 11:36:36.173 INFO 25340 --- [ntContainer#0-1] com.example.rabbitmq.consumer.Consumer : 接收到队列confirm.queue信息:发扣扣key1
2021-12-03 11:36:36.173 ERROR 25340 --- [ntContainer#3-1] c.e.rabbitmq.consumer.WarningConsumer : 报警发现不可路由信息:发扣扣key12
补充:mandatory参数和备份交换机可以一起使用时,如果两者同时开启,则备份交换机优先级更高,即只使用备份交换机,而不使用mandatory参数