RabbitMQ-消息队列:发布确认高级

18、发布确认高级

在生产环境中由于一些不明原因,导致 RabbitMQ 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?

1、确认机制方案

2、代码架构图

在配置文件当中需要添加

properties 复制代码
spring.rabbitmq.publisher-confirm-type=correlated
  • NONE 值是禁用发布确认模式 ,是默认值。

  • CORRELATED 值是发布消息成功到交换器后会触发回调方法

  • SIMPLE(最好不用) 值经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker。(相当于单一发布)

3、正常发送接收代码

(1)配置类

(2)生产者

(3)消费者

(4)测试效果:

4、回调接口

如果是交换机出了问题,队列出不出问题,都无济于事,交换机出了问题,交换机的缓存就没什么作用了,所以需要通过回调解决交换机宕机的问题

(1)修改生产者代码,添加回调接口方法

(2)查看源码

实现内部接口函数的注意点:

交换机确认回调方法

(4)创建回调函数的三部曲

发送方设置回调函数的CorrelationData

(5)配置开启交换机回调服务

测试效果:

交换机宕掉

测试效果:

结果:出现找不到交换机的错误

队列宕掉

测试效果:

访问: http://localhost:8080/confirm/sendMessage/LBJ


结果:会丢失RK对应的队列消息

可以看到,发送了两条消息,第一条消息的 RoutingKey 为 "key1",第二条消息的 RoutingKey 为 "key2",两条消息都成功被交换机接收 ,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息 ,所有第二条消息被直接丢弃了丢弃的消息交换机是不知道的,需要解决告诉生产者消息传送失败

代码实现

(1)配置类代码

java 复制代码
@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");
    }
}

(2)消息生产者的回调接口

java 复制代码
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
    /**
     * 交换机不管是否收到消息的一个回调方法
     *
     * @param correlationData 消息相关数据
     * @param ack             交换机是否收到消息
     * @param cause           为收到消息的原因
     */
    @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);
        }
    }

}

(3)消息生产者

java 复制代码
@RestController
@RequestMapping("/confirm")
@Slf4j
public class ProducerController {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private MyCallBack myCallBack;

    //依赖注入 rabbitTemplate 之后再设置它的回调对象
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(myCallBack);
    }
    
    /**
     * 消息回调和退回
     *
     * @param message
     */
    @GetMapping("sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {

        //指定消息 id 为 1
        CorrelationData correlationData1 = new CorrelationData("1");
        String routingKey = "key1";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData1);
        log.info(routingKey + "发送消息内容:{}", message + routingKey);

        CorrelationData correlationData2 = new CorrelationData("2");
        routingKey = "key2";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData2);
        log.info(routingKey + "发送消息内容:{}", message + routingKey);

    }

}

(4)消息消费者

java 复制代码
@Component
@Slf4j
public class ConfirmConsumer {
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    @RabbitListener(queues = CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message) {
        String msg = new String(message.getBody());
        log.info("接受到队列 confirm.queue 消息:{}", msg);
    }

}

5、回退消息

Mandatory 参数

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如 果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的 。那么如何 让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参 数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

(1)开启发布消息回退消息的核心配置

properties 复制代码
#消息退回
spring.rabbitmq.publisher-returns=true

(2)继承回调接口

(3)实现回调函数

java 复制代码
 @Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {

        log.error("消息{},被交换机{}退回,退回原因:{},路由Key:{}",new String(message.getBody()),exchange,replyText,routingKey);
    }

测试效果1:

报错原因:没有注入自定义的回调函数到接口里面

测试效果2:

6、备份交换机

有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法 ,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?

前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息 ,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息 。 在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。

备份交换机可以理解为 RabbitMQ 中交换机的 "备胎"当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理 ,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们①在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了 。当然,我们②还可以建立一个报警队列,用独立的消费者来进行监测和报警。

代码实现

(1)配置类中定义、声明交换机、队列

(2)按照架构图绑定交换机和队列

(3)设置备份交换机

java 复制代码
@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("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");
 }
 //声明备份 Exchange
 @Bean("backupExchange")
 public FanoutExchange backupExchange(){
 return new FanoutExchange(BACKUP_EXCHANGE_NAME);
 }
 //声明确认 Exchange 交换机的备份交换机
 @Bean("confirmExchange")
 public DirectExchange confirmExchange(){
 ExchangeBuilder exchangeBuilder =
 ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
 .durable(true)
//设置该交换机的备份交换机
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
 return (DirectExchange)exchangeBuilder.build();
 }
 // 声明警告队列
 @Bean("warningQueue")
 public Queue warningQueue(){
 return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
 }
 // 声明报警队列绑定关系
 @Bean
 public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
 @Qualifier("backupExchange") FanoutExchange 
backupExchange){
 return BindingBuilder.bind(queue).to(backupExchange);
 }
 // 声明备份队列
 @Bean("backQueue")
 public Queue backQueue(){
 return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
 }
 // 声明备份队列绑定关系
 @Bean
 public Binding backupBinding(@Qualifier("backQueue") Queue queue,
 @Qualifier("backupExchange") FanoutExchange backupExchange){
 return BindingBuilder.bind(queue).to(backupExchange);
 }
}

(4)报警消费者(转发消费者自己拓展)

测试效果:

测试之前:

去RabbitMQ的web管理界面删除已经建立的交换机,否则会和将要建立的交换机和备用交换机起冲突

重新启动项目的时候需要把原来的 confirm.exchange 删除因为我们修改了其绑定属性

备用交换机成功路由

小结

mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先 级高,经过上面结果显示答案是备份交换机优先级高

回退消息的优先级低

RabbitMQ-消息队列:发布确认高级 到此完结,笔者归纳、创作不易,大佬们给个3连再起飞吧

相关推荐
舒克日记1 小时前
基于springboot的民谣网站的设计与实现
java·spring boot·后端
lang201509283 小时前
Spring Boot配置属性:类型安全的最佳实践
spring boot
koping_wu7 小时前
【RabbitMQ】架构原理、消息丢失、重复消费、顺序消费、事务消息
分布式·架构·rabbitmq
Jabes.yang9 小时前
Java面试场景:从Spring Web到Kafka的音视频应用挑战
大数据·spring boot·kafka·spring security·java面试·spring webflux
喵桑..9 小时前
kafka源码阅读
分布式·kafka
酷ku的森11 小时前
RabbitMQ的概述
分布式·rabbitmq
程序员小凯11 小时前
Spring Boot性能优化详解
spring boot·后端·性能优化
tuine12 小时前
SpringBoot使用LocalDate接收参数解析问题
java·spring boot·后端
番茄Salad13 小时前
Spring Boot项目中Maven引入依赖常见报错问题解决
spring boot·后端·maven
程序员小凯13 小时前
Spring MVC 分布式事务与数据一致性教程
分布式·spring·mvc