RabbitMQ 学习(七)-- 高级发布确认

高级发布确认(生产者角度)

在生产环境中由于一些不明原因,导致 RabbitMQ 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?

1、发布确认机制方案

  • 应答机制传回消息是 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 实战

2、消息确认

2.1、配置文件

配置文件中添加

spring.rabbitmq.publisher-confirm-type=correlated 
  • NONE

    禁用发布确认模式,是默认值

  • CORRELATED

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

  • SIMPLE

    在发布消息成功后使用 RabbitTemplate 调用 waitForConfirms()waitForConfirmsOrDie() 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie() 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broke

2.2、配置类代码

/**
 * @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);
    }
}

2.3、回调接口

@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);
        }
    }
}

2.4、生产者代码

@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);
    }
}

2.5、消费者代码

@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);
    }
}

2.6、运行测试与问题

第三条消息会被直接丢弃,生产者不能得知,如何解决?消息回退模式

在这里插入图片描述

3、消息回退

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

3.1、配置文件

spring:
  rabbitmq:
   publisher-returns: true

3.2、回调接口

@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);
    }
}

3.3、运行测试

在这里插入图片描述

4、交换机备份

​ 有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然 后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增 加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢? 前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些 处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。

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

4.1、架构图

在这里插入图片描述

4.2、配置类

@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);
    }
}

4.3、消费者

@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);
    }
}

4.5、生产者代码

使用消息确认的生产者代码

4.6、启动测试

注意:重新启动项目的时候需要把原来的 confirm.exchange 删除因为我们修改了其绑定属性,不然会报错

发送消息测试:http://localhost:8080/confirm/sendMsg/Hello%20World

在这里插入图片描述

mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,谁优先级高,答案是备份交换机优先级高

猜你喜欢

转载自blog.csdn.net/weixin_43989102/article/details/126295602