本文已参与【新人创作礼】活动,一起开启掘金创作之路。
目录
一、使用场景
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。
需要进行 RabbitMQ 的消息可靠投递,用到了发布确认模式。
二、原理
确认机制方案
编辑
代码架构图
编辑
三、代码实现
1.yml文件
spring:
rabbitmq:
host: 192.168.16.106
port: 5672
username: guest
password: guest
publisher-confirm-type: correlated
1)NONE
禁用发布确认模式,是默认值
2) CORRELATED
发布消息成功到交换器后会触发回调方法3) SIMPLE 同步模式
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,
其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是
waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
2.配置类代码
@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_KRY = "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 confirmQueueBindingExchange(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange directExchange){
return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KRY);
}
}
3.实现回调接口
@Component //第一步
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback{
@Autowired //第二步
private RabbitTemplate rabbitTemplate;
//注入
@PostConstruct //第三步
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* 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) {
if(ack){
log.info(" 交换机已经收到 id 为:{} 的消息",correlationData.getId());
}else{
log.info(" 交换机还未收到 id 为:{} 消息, 由于原因:{}",correlationData.getId(),cause);
}
}
}
4.生产者代码
@RestController
@Slf4j
public class ProduceController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message){
CorrelationData correlationData = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KRY,
message+"1",correlationData);
log.info("发送的消息:"+message+"1");
CorrelationData correlationData2 = new CorrelationData("2");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KRY+"2",message+"2",correlationData2);
log.info("发送的消息:"+message+"2");
}
}
5.消费者代码
@Component
@Slf4j
public class ConfirmConsumer {
//接收消息
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receive(Message message, Channel channel) throws Exception{
String msg = new String(message.getBody());
log.info("收到confirm.queue队列的消息:{}",msg);
}
}
编辑
可以看到,发送了两条消息,第一条消息的 RoutingKey 为 "key1",第二条消息的 RoutingKey 为
"key12",两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有第二条消息被直接丢弃了。
四、回退消息
1.Mandatory 参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
2.修改回调接口代码,其余不变
@Component //第一步
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback{
@Autowired //第二步
private RabbitTemplate rabbitTemplate;
//注入
@PostConstruct //第三步
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(this);
}
/**
* 交换机确认回调方法
* 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) {
if(ack){
log.info(" 交换机已经收到 id 为:{} 的消息",correlationData.getId());
}else{
log.info(" 交换机还未收到 id 为:{} 消息, 由于原因:{}",correlationData.getId(),cause);
}
}
//可以在当消息传递过程中不可达目的地时将消息返回给生产者
//只有不可达目的地的时候,才进行回退
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息{},被交换机{}退回,退回原因:{},路由key:{}",
new String(message.getBody()),exchange,replyText,routingKey);
}
}
3.运行结果
编辑
五、备份交换机
1.概念
备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
2.使用范围
添加处理这些被退回的消息的逻辑,可以做到既不丢失消息,又不增加生产者的复杂性。
3.原理图
编辑
4.配置类文件
@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_KRY = "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("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
@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 confirmQueueBindingExchange(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange directExchange){
return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KRY);
}
@Bean
public Binding warningQueueBindingExchange(@Qualifier("backupQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
@Bean
public Binding backQueueBindingExchange(@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
}
5.warning消费者
@Component
@Slf4j
public class WarningConsumer {
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receive(Message message){
String msg = new String(message.getBody());
log.info("报警发现不可路由消息:{}",msg);
}
}
6.运行代码
重新启动项目的时候需要把原来的 confirm.exchange 删除因为我们修改了其绑定属性,不然报以下错:
编辑
编辑
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。