当生产者在发布消息时,如果交换机由于某些原因宕机或者其他原因没有收到消息或者没有将消息发送给队列时
这时为了保证消息的不丢失,以便交换机恢复正常后生产者可以重新发布消息
发布消息
配置类
书写发布确认配置类:
Configuration
public class ConfirmConfig {
//交换机名称
public static final String CONFIRM_EXCHANGE_NAME = "confirm_exchange";
//队列名称
public static final String CONFIRM_QUEUE_NAME = "confirm_queue";
//routingKey 路由Key
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 confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
}
}
书写生产者:
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发消息
*/
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message){
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
ConfirmConfig.CONFIRM_ROUTING_KEY,message);
log.info("发送消息内容:{}",message);
}
}
书写消费者:
@Slf4j
@Component
public class Consumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveConfirmMessage(Message message){
String msg = new String(message.getBody());
log.info("接收到的队列confirm.queue消息:{}",msg);
}
}
启动项目,在浏览器地址栏中输入:
http://localhost:8080/confirm/sendMessage/你好啊世界
运行结果:
此时交换机等都是正常正确的运行结果,如果生产者在发送消息时交换机的名称传参错误或者队列名称填入错误,那么消费者将会收不到生产者发送的消息
此时我们需要书写一个自定义实现类,实现RabbitmqTemplate内部接口ConfirmCallback
书写自定义实现类:MyCallBack
Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
//由于此处MyCallBack 实现的是RabbitmqTemplate的内部接口 ConfirmCallback
//所以此处RabbitmqTemplate想要调用我们的MyCallBack时需要将其注入到RabbitmqTemplate的ConfirmCallback中
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* 发消息 交换机收到消息 回调
* @param correlationData 保存回调消息的id及相关信息
* @param b 交换机是否收到消息 收到:true 未收到: false
* @param cause 交换机未收到消息的原因 收到消息: null 未收到消息:具体的消息内容
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String cause) {
String id = correlationData!=null ? correlationData.getId() : "";
if(b){
log.info("交换机已经收到的id为:{}的消息",id);
}else{
log.info("交换机还未收到id为:{}的消息,没有收到消息的原因是:{}",id,cause);
}
}
}
此时我们还需要在配置文件application.properties中添加:spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-confirm-type=correlated
NONE : 禁用发布确认模式 默认
CORRELATED: 发布消息成功后到交换器后会触发回调方法
SIMPLE: 经测试会有两种效果
效果一: 和correlated一样会触发回调方法
效果二: 在发布消息成功后使用rabbitTemplate调用waitForConfims或waitForConfirsOrDie方法等待briker节点返回发送结果 根据返回结果来判定下一步的逻辑 要注意的是waitForConfimsOrDie方法如果返回false则会关闭channel 则接下来无法发送消息到broker(代理)
验证结果
在生产者中进行些许改动:
此处将浇交换机名称我们故意拼接"11",然后再运行看下接收到的结果,运行后浏览器仍然输入:http://localhost:8080/confirm/sendMessage/你好啊世界
查看结果:
报错结果图片截不到全部,以文字形式放在下面:
[nio-8080-exec-2] c.s.r.s.controller.ProducerController : 发送消息内容:你好啊世界
ERROR 9060 — [226.11.176:5672] o.s.a.r.c.CachingConnectionFactory : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange ‘confirm_exchange11’ in vhost ‘/’, class-id=60, method-id=40)
INFO 9060 — [nectionFactory2] c.s.r.s.config.MyCallBack : 交换机还未收到id为:1的消息,没有收到消息的原因是:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange ‘confirm_exchange11’ in vhost ‘/’, class-id=60, method-id=40)
此处交换机未收到id为1的消息,原因是没有找到名字为confirm_exchange11的交换机
下面再进行改动,将交换机名称改为正常,而交换机和队列之间的路由key做一些手脚,故意使其路由key错误
还是在生产者中进行改动:
运行项目,浏览器输入和前次测试同样的路径,查看运行结果:
所以此时如果交换机出现问题或者队列或路由key出现问题,都应该进行回调接口,回调接口进行之后应该再对消息进行回退处理
回退消息
在只开启了生产者确认机制的情况下, 交换机接收到消息后,会直接给消息生产者发送确认消息,如果发小该消息不可路由,那么消息会直接被丢弃,此时生产者是不知道消息被丢弃这个事件的 那么我们可以通过设置Mandatory这个参数可以在当消息传递过程中不可达目的地时将消息返回给生产者
在前面写的自定义回调接口实现类中添加新的实现:RabbitTemplate.ReturnCallback
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
//由于此处MyCallBack 实现的是RabbitmqTemplate的内部接口 ConfirmCallback
//所以此处RabbitmqTemplate想要调用我们的MyCallBack时需要将其注入到RabbitmqTemplate的ConfirmCallback中
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 交换机确认回调方法
* 发消息 交换机收到消息 回调
* @param correlationData 保存回调消息的id及相关信息
* @param b 交换机是否收到消息 收到:true 未收到: false
* @param cause 交换机未收到消息的原因 收到消息: null 未收到消息:具体的消息内容
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String cause) {
String id = correlationData!=null ? correlationData.getId() : "";
if(b){
log.info("交换机已经收到的id为:{}的消息",id);
}else{
log.info("交换机还未收到id为:{}的消息,没有收到消息的原因是:{}",id,cause);
}
}
//可以在当消息传递过程中不可达目的地时将消息返回给生产者
//只有不可达目的地的时候 才进行回退
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("消息{},被交换机{}退回,退回原因{},路由key{}",
new String(message.getBody()),exchange,replyText,routingKey);
}
}
我们还需要在配置文件application.properties中添加:spring.rabbitmq.publisher-returns=true
运行项目,在浏览器输入访问路径,查看运行结果:
备份交换机
有了mandatory参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性该怎么做呢?在设置死信队列的时可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在RabbitMQ中,有一种备份交换机的机制存在,可以很好的应对这个问题。备份交换机可以理解为RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一 个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到- -条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立- -个报警队列,用独立的消费者来进行监测和报警。
在确认配置类中添加备份交换机配置:
@Configuration
public class ConfirmConfig {
//交换机
public static final String CONFIRM_EXCHANGE_NAME = "confirm_exchange";
//队列
public static final String CONFIRM_QUEUE_NAME = "confirm_queue";
//routingKey
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("confirmExchange")
public DirectExchange confirmExchange() {
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 queueBindingExchange(@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
}
//声明备份交换机
@Bean("backupExchange")
public FanoutExchange backupExchange() {
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//声明备份队列
@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
public Binding backupQueueBindingBackupExchange(@Qualifier("backupQueue") Queue backupQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
//绑定备份交换机和报警队列
@Bean
public Binding warningQueueBindingBackupExchange(@Qualifier("warningQueue") Queue warningQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
书写报警消费者:
@Component
@Slf4j
public class WarningConsumer {
//接收报警消息
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message){
String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}",msg);
}
}
生产者依旧使用之前回退消息中路由key一个正常一个异常的生产者即可
注意,此时我们的发布确认交换机的配置类已经发生改变,所以我们需要在rabbitmq的网页端管理中删除原先的confirm_exchage,然后再启动项目才可以
重启项目,仍然在浏览器中输入访问路径,观察后台结果:
这时我们会发现,路由key有异常的key2由备份交换机发送给了报警队列,进而由报警消费者消费了消息
当我们回退消息与备份交换机一起使用的时候,如果两者同时开启,经过上面结果显示答案是备份交换机优先级高