消费方消息确认篇链接
在我的另一篇消费方的消息确认篇说明了,如何应对队列在发送消息给消费者的过程中出现了问题导致消费者无法消费,那么生产者发送消息给交换机的过程中出现了问题,以及交换机发送消息给队列的过程中出现了问题,特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候应该如何应对?
每一个颜色块之间都存在着消息的确认机制,我们大概分为两大类,发送方确认和消费方确认,其中发送方确认又分为生产者到交换器的确认和交换器到队列的确认。
接下来我们主要讨论发送方确认
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
配置文件
spring.rabbitmq.host=192.168.136.128
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
spring.rabbitmq.publisher-confirm-type=correlated
开启交换机消息确认需要在配置文件中添加
spring.rabbitmq.publisher-confirm-type
NONE
禁用发布确认模式,是默认值
CORRELATED
发布消息到交换器后会触发回调方法,无论交换机是否收到消息
SIMPLE
存在关闭信道的风险,所以不常使用
添加配置类
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//声明业务 Exchange
@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 queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
}
生产者
@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
public static final String CONFIRM_EXCHANGE_NAME = ConfirmConfig.CONFIRM_EXCHANGE_NAME ;
@Autowired
private RabbitTemplate rabbitTemplate;
private String routingKey = "key1";
@GetMapping("sendMessage")
public void sendMessage(){
//指定消息 id 为 1
CorrelationData correlationData1=new CorrelationData("1");
String message = "大家好1key1";
String message2 = "大家好1key12";
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey,message,correlationData1);
log.info("发送消息内容:{}",message);
//指定不存在的routingKey发送一条信息
CorrelationData correlationData2=new CorrelationData("2");
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey+"2",message2,correlationData2);
log.info("发送消息内容:{}",message2);
}
}
生产者发送消息时需要设置CorrelationData(),改类是回调接口中消息的相关信息。
CorrelationData有两个字段分别
一个是消息的id,一个是消息体
实现消息确认接口
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机不管是否收到消息的一个回调方法
* CorrelationData
* 消息相关数据
* ack
* 交换机是否收到消息
* cause
* 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause){
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}
}
}
因为回调接口实现的是RabbitTemplate中的内部类,所以要通过**rabbitTemplate.setConfirmCallback(this);**将我们的实现类给注入到内部类中。
消费者
@Component
@Slf4j
public class ConfirmConsumer {
public static final String CONFIRM_QUEUE_NAME = ConfirmConfig.CONFIRM_QUEUE_NAME;
@RabbitListener(queues =CONFIRM_QUEUE_NAME)
public void receiveMsg(Message message){
String msg=new String(message.getBody());
log.info("接受到队列 confirm.queue 消息:{}",msg);
}
}
结果展示
调用/confirm/sendMessage针对结果我们可以看到,key1发送成功并且被交换机与队列成功接受消费掉,key2由于指定的routingKey不存在,所以交换机没有真正接受到消息,所以未派送给队列成功消费,但还是成功进行了回调!
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,但如果该消息无法传送到队列中,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。
因此我们可以通过设置 mandatory 参数,当交换机发送消息给队列时如果出现异常,导致队列未收到消息,那么就将改消息退回到RabbitTemplate.ReturnCallback中的returnedMessage()消息回退接口中。
实现消息回退接口RabbitTemplate.ReturnCallback,并注入到内部类
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 重写消息确认回调接口
* 交换机不管是否收到消息的一个回调方法
* CorrelationData
* 消息相关数据
* ack
* 交换机是否收到消息
* cause
* 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause){
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}
}
/**
*重写消息回退接口
*该接口只在交换机无法将消息送达到队列时,进行回调
*
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息:{}被交换机{}退回,退回原因:{},路由 key:{}",new String(message.getBody()), exchange, replyText,routingKey);
}
}
然后运行生产者
观察结果
通过结果可以发现,key2由于routingKey不存在,无法送到队列中,所以消息被退回。
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
我们根据上图在原来代码的基础上,添加一个备份交换机丶两个队列丶两个消费者以及它们的绑定关系,因为这里交换机的类型是fanout会将消息发送给所有队列因此我们就只写一个消费者即可。
修改配置类
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//新增备份交换机
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("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("confirmExchange")
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange()
//持久化交换机
.durable(true)
//设置该交换机的备份交换机
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME)
.build();
}
//新增备份交换机与备份队列的绑定关系
@Bean
public Binding queueBinding(@Qualifier(BACKUP_QUEUE_NAME) Queue queue,
@Qualifier(BACKUP_EXCHANGE_NAME) FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("");
}
//新增备份交换机与报警队列的绑定关系
@Bean
public Binding queueBinding(@Qualifier(WARNING_QUEUE_NAME) Queue queue,
@Qualifier(BACKUP_EXCHANGE_NAME) FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("");
}
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
// 声明确认队列绑定关系
@Bean
public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
}
ps: 因为修改了confirmExchange交换机,所以如果之前存在该交换机,需要先将该机删除。
新增报警消费者
@Component
@Slf4j
public class WarningConsumer {
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveMsg(Message message){
String msg=new String(message.getBody());
log.info("报警发现不可路由消息:{}",msg);
}
}
可以发现由于大家好key12由于routingKey不存在,就直接转发到了备份交换机然后成功背报警消费者消费到了。
备份交换机与ReturnCallback的优先级
这里需要注意,我们同时设置了备份交换机和实现了ReturnCallback消息回退接口,当交换机发送消息给队列是发生了不可路由的情况,消息是被转发到了备份交接中,因此可以证明备份交换机的优先级比ReturnCallback高。
小结:
至此