发布确认实现
之前提到的发布确认机制,即 channel.addConfirmListener()
(异步发布确认)是 rabbitmq 原生 Java 客户端实现的消息发布确认回调,现在使用的是 springboot,可以通过实现 Spring AMQP 提供的 ConfirmCallback
接口,用于处理消息的确认回调,该接口是异步接口
复习概念
由于某种原因(rabbitmq 重启、宕机…),导致生产者消息投递到交换机或者队列之前,消息就已经丢失了,此时需要手动处理和恢复,处理方案即使用发布确认机制,实现对消息发送结果的实时监控和处理
交换机挂掉
交换机接收不到消息时进行的处理
确认机制方案
代码架构图
配置文件
在配置文件中需设置 publisher-confirm-type
为 correlated
spring:
rabbitmq:
port: 5672
publisher-confirm-type: correlated
- none:默认值,表示禁用发布确认模式,是默认值
- correlated:发布消息成功到交换器后会触发回调方法
- simple:有 2 个效果,如下:
- 跟 correlated 一样会触发回调方法
- 发布消息成功后可以使用 rabbitTemplate 调用
waitForConfirms()
或waitForConfirmsOrDie()
来等待消息的发布确认结果,这两个方法都会阻塞当前线程,是同步操作,如果使用waitForConfirmsOrDie()
方法,并返回false
,它将会关闭通道,导致后续无法发送消息到 RabbitMQ
声明
@Configuration
public class ConfirmConfig {
// 声明交换机
@Bean
public DirectExchange confirmExchange() {
return new DirectExchange("confirm_xchange");
}
// 声明队列
@Bean
public Queue confirmQueue() {
return QueueBuilder.durable("confirm.queue").build();
}
// 绑定
@Bean
public Binding queueBindingExchange(
@Qualifier("confirmExchange") DirectExchange confirmExchange,
@Qualifier("confirmQueue") Queue confirmQueue) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with("key1");
}
}
生产者
@RestController
@RequestMapping("/confirm")
public class ProducerController {
@Resource
private RabbitTemplate rabbitTemplate;
private static final Logger log = LoggerFactory.getLogger(ProducerController.class);
// 发消息
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message) {
rabbitTemplate.convertAndSend("confirm_exchange",
"key1", message);
log.info("发送消息内容:{}", message);
}
}
消费者
@Component
public class Consumer {
private static final Logger log = LoggerFactory.getLogger(Consumer.class);
@RabbitListener(queues = "confirm.queue")
public void receiveConfirmMessage(Message message) {
String msg = new String(message.getBody());
log.info("队列 confirm.queue 的消息是:{}", msg);
}
}
回调接口
以上代码是正常的发送消息和消费消息,现在模拟交换机挂了,使用回调接口,该接口作用如下:
- 当生产者发消息给交换机后成功了会触发回调函数
- 当生产者发消息给交换机后失败了也会触发回调函数
代码如下:
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
private static final Logger log = LoggerFactory.getLogger(MyCallBack.class);
/**
* 交换机确认消息的回调方法
* 什么时候会回调:
* 1.发消息,交换机成功接收到了
* 1.1 correlationData 保存回调消息的 ID 及相关信息
* 1.2 交换机是否收到消息,true
* 1.3 失败原因,null
* 2.发消息,交换机接收失败了
* 2.1 correlationData 保存回调消息的 ID 及相关信息
* 2.2 交换机是否收到消息,false
* 2.3 失败原因
*/
@Override
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);
}
}
// 本类只是单纯实现了 RabbitTemplate 的内部接口,并不会
// 作用于 RabbitTemplate,需注入到 RabbitTemplate 中
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* @PostConstruct 作用如下:
* Spring 容器实例化 Bean 并完成依赖注入后立即执行该方法
*/
@PostConstruct
public void init(){
// 注入
rabbitTemplate.setConfirmCallback(this);
}
}
该回调接口的 correlationData 参数需要生产者发送消息时发送,之前的生产者并没有发送 CorrelationData 对象,修改后的生产者代码如下:
@RequestMapping("/confirm")
public class ProducerController {
@Resource
private RabbitTemplate rabbitTemplate;
private static final Logger log = LoggerFactory.getLogger(ProducerController.class);
// 发消息
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message) {
CorrelationData correlationData = new CorrelationData("1");
// 故意写错交换机的名字,模拟交换机挂了
rabbitTemplate.convertAndSend("confirm_exchange666",
"key1", message, correlationData);
log.info("发送消息内容:{}", message);
}
}
队列挂掉
mandatory
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者并不知道消息已经被丢弃,通过设置 mandatory
参数可以在当消息传递过程中不可达目的地时将消息返回给生产者
配置文件
添加 publisher-returns
为 true
,表示开启消息回退,当消息无法被路由到合适的队列时,RabbitMQ 会将该消息退回给生产者
spring:
rabbitmq:
port: 5672
publisher-confirm-type: correlated
publisher-returns: true
回退接口
@Component
public class MyCallBack2 implements RabbitTemplate.ReturnCallback{
private static final Logger log = LoggerFactory.getLogger(MyCallBack2.class);
// 本类只是单纯实现了 RabbitTemplate 的内部接口,并不会
// 作用于 RabbitTemplate,需注入到 RabbitTemplate 中
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* @PostConstruct 作用如下:
* Spring 容器实例化 Bean 并完成依赖注入后立即执行该方法
*/
@PostConstruct
public void init(){
// 注入
rabbitTemplate.setReturnCallback(this);
/**
* 设置 mandatory
* true:交换机无法将消息进行路由时,会将该消息返回给生产者
* false:如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);
}
// 当消息不可达目的地时才进行回退
@Override
public void returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey) {
log.error("消息:{},被交换机{}退回,退回原因是{},路由key是{}",
new String(message.getBody()), exchange, replyText, routingKey);
}
}
备份交换机
有了 mandatory 参数和回退接口后,我们获得了对投递消息失败后的感知能力,能在生产者的消息无法被投递时发现并处理,但就需要在生产者中添加处理这些被退回的消息的逻辑,从而增加了生产者的复杂度
如果既不想丢失消息,又不想增加生产者的复杂性,可以使用备份交换机,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout
代码架构图
在备份交换机中绑定 1 个备份队列和 1 个报警队列,用独立的消费者对报警队列进行监测和报警
声明
在之前代码中新增 1 个备份交换机和 2 个队列,代码如下:
@Configuration
public class ConfirmConfig {
// 声明交换机
@Bean
public DirectExchange confirmExchange() {
return ExchangeBuilder.directExchange("confirm_exchange").durable(true)
.withArgument("alternate-exchange", "backup.exchange") // 绑定备份交换机
.build();
}
// 声明队列
@Bean
public Queue confirmQueue() {
return QueueBuilder.durable("confirm.queue").build();
}
// 绑定
@Bean
public Binding queueBindingExchange(
@Qualifier("confirmExchange") DirectExchange confirmExchange,
@Qualifier("confirmQueue") Queue confirmQueue) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with("key1");
}
// 声明备份交换机
@Bean
public FanoutExchange backupExchange() {
return new FanoutExchange("backup.exchange");
}
// 声明备份队列
@Bean
public Queue backupQueue() {
return QueueBuilder.durable("backup.queue").build();
}
// 声明报警队列
@Bean
public Queue warningQueue() {
return QueueBuilder.durable("warning.queue").build();
}
// 绑定备份交换机和备份队列
@Bean
public Binding backupQueueBindingBackup(
@Qualifier("backupExchange") FanoutExchange backupExchange,
@Qualifier("backupQueue") Queue backupQueue) {
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
// 绑定备份交换机和报警队列
@Bean
public Binding backupQueueBindingWarning(
@Qualifier("backupExchange") FanoutExchange backupExchange,
@Qualifier("warningQueue") Queue waringQueue) {
return BindingBuilder.bind(waringQueue).to(backupExchange);
}
}
消费者
@Component
public class WarningConsumer {
private static final Logger log = LoggerFactory.getLogger(WarningConsumer.class);
// 接收报警消息
@RabbitListener(queues = "warning.queue")
public void receiveWarningMsg(Message message){
String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}", msg);
}
}
mandatory 参数与备份交换机可以一起使用时,回退接口不会执行,备份交换机的优先级高