高级发布确认(生产者角度)
在生产环境中由于一些不明原因,导致 RabbitMQ 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?
应答机制传回消息是 broker, 确认机制是消息到队列后,传回确定消息给 producer,现在 broker 坏了,无法进行消息确认,所以考虑现在这种方法解决,这里针对的 producer 的角度。
架构:当生产者投递消息失败了,就把消息存入到缓存当中,然后使用定时任务重新发送,交换机接收到消息了就删除缓存。
确认模式:消息从
生产者 到 交换机
则会返回一个ConfirmCallback
。回退模式:消息从
交换机 到 队列
投递失败则会返回一个ReturnCallback
。
确认模式: (生产者和交换机的问题)
步骚:
1、配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
2.实现
RabbitTemplate.ConfirmCallBack
接口
回退模式: (交换机和队列的问题)
当消息发送给 Exchange 后,Exchange 路由到 Queue 失败是才会执行 ReturnCallBack
步骤:
1、配置文件当中需要添加
spring.rabbitmq.publisher-returns=true
2、实现
RabbitTemplate.ReturnCallback
接口
基于 SpringBoot 实战
配置文件中添加
spring.rabbitmq.publisher-confirm-type=correlated
NONE
禁用发布确认模式,是默认值
CORRELATED
发布消息成功到交换器后会触发回调方法
SIMPLE
在发布消息成功后使用 RabbitTemplate
调用 waitForConfirms()
或 waitForConfirmsOrDie()
方法等待 broker
节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie()
方法如果返回 false
则会关闭 channel
,则接下来无法发送消息到 broke
。
/**
* @desc
* @auth llp
* @date 2022年08月10日 23:47
*/
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE = "confirm.exchange";
public static final String CONFIRM_QUEUE = "confirm.queue";
public static final String CONFIRM_ROUTING_KEY = "confirm.routing.key";
@Bean
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE);
}
@Bean
public Queue confirmQueue(){
return new Queue(CONFIRM_QUEUE);
}
@Bean
public Binding queueBingExchange(
@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
}
}
@Slf4j
@Component // 第一步注入
public class ConfirmCallBack implements RabbitTemplate.ConfirmCallback {
@Resource // 第二步注入
private RabbitTemplate rabbitTemplate;
// @PostConstruct 注解,在对象加载完依赖注入后执行它通常都是一些初始化的操作,
// 但初始化可能依赖于注入的其他组件,所以要等依赖全部加载完再执行
// 第三步注入
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* @desc 交换机确认回调方法, 发消息 交换机接收到消息触发回调函数
* @auth llp
* @date 2022/8/11 22:40
* @param correlationData 保存回调消息的 ID 及相关信息
* @param ack 交换机是否收到消息
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
log.info("交换机已收到ID为:{} 的消息, cause:{}", correlationData.getId(), cause);
}else {
log.info("交换机未收到ID为:{} 的消息, cause:{}", correlationData.getId(), cause);
}
}
}
@Slf4j
@RestController
public class ConfirmController {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/confirm/sendMsg/{message}")
public void sendMessage(@PathVariable String message) throws InterruptedException {
// 交换机和路由键都正确
CorrelationData correlationData1 = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE, ConfirmConfig.CONFIRM_ROUTING_KEY, message, correlationData1);
log.info("发送的消息内容为:{}", message);
TimeUnit.SECONDS.sleep(1);
// 交换机不正确,路由键正确
CorrelationData correlationData2 = new CorrelationData("2");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE+"ddd", ConfirmConfig.CONFIRM_ROUTING_KEY, message, correlationData2);
log.info("发送的消息内容为:{}", message);
TimeUnit.SECONDS.sleep(1);
// 交换机正确,路由键不正确(找不到队列)
CorrelationData correlationData3 = new CorrelationData("3");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE, ConfirmConfig.CONFIRM_ROUTING_KEY+"ddd", message, correlationData3);
log.info("发送的消息内容为:{}", message);
}
}
@Slf4j
@Component
public class ConfirmConsumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE)
public void receiveQueueDead(Message message, Channel channel){
String msg = new String(message.getBody());
log.info("当前时间:{}, 接收到消息为:{}", new Date(), msg);
}
}
第三条消息会被直接丢弃,生产者不能得知,如何解决?消息回退模式
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。 那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory
参数 可以在当消息传递过程中不可达目的地时将消息返回给生产者。
spring:
rabbitmq:
publisher-returns: true
@Slf4j
@Component // 第一步注入
public class ConfirmCallBack implements RabbitTemplate.ReturnCallback {
@Resource // 第二步注入
private RabbitTemplate rabbitTemplate;
// @PostConstruct 注解,在对象加载完依赖注入后执行它通常都是一些初始化的操作,
// 但初始化可能依赖于注入的其他组件,所以要等依赖全部加载完再执行
// 第三步注入
@PostConstruct
public void init(){
rabbitTemplate.setReturnCallback(this);
}
// 只有消息不可路由才会调用此函数,成功不调用
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("消息 {}, 被交换机 {} 退回, 应答代码 {}, 原因 {}, 路由 {}",
new String(message.getBody()), exchange, replyCode, replyText, routingKey);
}
}
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然 后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增 加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢? 前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些 处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就 是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备 份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进 入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE = "confirm.exchange";
public static final String BACKUP_EXCHANGE = "backup.exchange";
public static final String CONFIRM_QUEUE = "confirm.queue";
public static final String BACKUP_QUEUE = "backup.queue";
public static final String WARNING_QUEUE = "warning.queue";
public static final String CONFIRM_ROUTING_KEY = "key1";
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE).build();
}
// 声明确认队列
@Bean("backupQueue")
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_QUEUE).build();
}
// 声明警告队列
@Bean("warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE).build();;
}
// 声明确认 Exchange 交换机的备份交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE)
.durable(true)
// 设置该交换机的备份交换机
.withArgument("alternate-exchange", BACKUP_EXCHANGE)
.build();
}
// 声明备份 Exchange
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE);
}
// 声明确认队列绑定关系
@Bean
public Binding confirmQueueBingExchange(
@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
}
// 声明备份队列绑定关系
@Bean
public Binding backupQueueBindExchange(
@Qualifier("backupQueue") Queue backupQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
// 声明报警队列绑定关系
@Bean
public Binding warningQueueBindExchange(
@Qualifier("warningQueue") Queue warningQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
@Slf4j
@Component
public class WarningConsumer {
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE)
public void receiveQueueDead(Message message, Channel channel){
String msg = new String(message.getBody());
log.info("warning consumer 收到不可路由的消息为:{}", msg);
}
}
使用消息确认的生产者代码
注意:重新启动项目的时候需要把原来的 confirm.exchange 删除因为我们修改了其绑定属性,不然会报错
发送消息测试:http://localhost:8080/confirm/sendMsg/Hello%20World
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,谁优先级高,答案是备份交换机优先级高。