rabbitmq之备份交换机

前言

// 处理成功----false 表示不批量处理,批量处理容易丢失信息,消息会被rabbitmq broker 删除。
channel.basicAck(envelope.getDeliveryTag(), false);

// 处理失败 重新入队
channel.basicNack(envelope.getDeliveryTag(), false, true);
// 放弃消息,不重新入队,此时相当于消息被拒,配置了死信交换机就会存放到死信中
channel.basicNack(tag, false, false);
// 重新入队
channel.basicNack(tag, false, true);

有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息 无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然 后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者 所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增 加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的 复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些 处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。 在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时, 就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由 备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑 定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都 进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

代码架构图

image
若正常交换机及队列出现问题,消息将被备用交换机接收并转发,转发到备份队列进行备份,以及转发到报警队列进行报警提醒

配置类

针对与普通交换转发给备份交换机需要携带此参数
arguments.put("alternate-exchange",BACKUP_EXCHANGE);


/**
 * 备份交换机
 */
@Configuration
public class BackupsConfig {

    // 声明普通交换机
    private static final String BOOT_CFM_EXC = "boot.cfm.exc";
    // 声明普通队列
    private static final String BOOT_QUEUE = "boot.queue";
    // 声明 routing-key
    private static final String ROUTING_KEY = "key1";
    // 声明备份交换机
    public static final String  BACKUP_EXCHANGE = "backup.exchange";
    // 声明备份队列
    public static final String BACKUP_QUEUE = "backup.queue";
    // 声明警告队列
    public static final String WARNING_QUEUE = "warning.queue";


    // 声明普通交换机--->若交换机无法正常将消息传递给消费者,那么将消息转存到备份交换机中
    @Bean
    public DirectExchange crmExchange(){
        Map<String,Object> arguments = new HashMap<>();
        // 绑定关系,如果交换机无法发送给队列,那么转发到备份交换机中,与之前的 死信队列 类似
        arguments.put("alternate-exchange",BACKUP_EXCHANGE);
        return new DirectExchange(BOOT_CFM_EXC,true,false,arguments);
    }

    // 声明普通队列
    @Bean
    public Queue crmQueue(){
        return QueueBuilder.durable(BOOT_QUEUE).build();
    }

    // 绑定关系--->将普通交换机和 普通队列进行关联
    @Bean
    public Binding bindCrmExchangeAndCrmQueue(@Qualifier("crmExchange") DirectExchange crmExchange,
                                              @Qualifier("crmQueue") Queue crmQueue){
        return BindingBuilder.bind(crmQueue).to(crmExchange).with(ROUTING_KEY);
    }

    // 声明备份交换机
    @Bean
    public FanoutExchange backExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE);
    }

    // 声明备份队列
    @Bean
    public Queue backQueue(){
        return QueueBuilder.durable(BOOT_QUEUE).build();
    }

    // 声明警告队列
    @Bean
    public Queue warningQueue(){
        return QueueBuilder.durable(WARNING_QUEUE).build();
    }

    //绑定关系。备份交换机和备份队列
    @Bean
    public Binding bindingBackExchangeAndBackQueue(@Qualifier("backExchange")FanoutExchange backExchange,
                                                   @Qualifier("backQueue") Queue backQueue){
        return BindingBuilder.bind(backQueue).to(backExchange);
    }

    //绑定关系。备份交换机和警告队列
    @Bean
    public Binding bindingBackExchangeAndWarningQueue(@Qualifier("backExchange")FanoutExchange backExchange,
                                                   @Qualifier("warningQueue") Queue warningQueue){
        return BindingBuilder.bind(warningQueue).to(backExchange);
    }
}

发布确认回调接口

自定义一个类分别实现:RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback
这两个接口,其中ConfirmCallback 接口是交换机的确认回调,ReturnCallback 是消息回调,当消息传送到队列过程中不可抵达的时候 将消息返回给生产者

在实现两个接口后,我们需要将它注入到我们的rabbitTemplate中:

   /**
     * 向rabbitTemplate 注入回调失败的类
     * 后置处理器:其他注解都执行结束才执行。
     */
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

完整代码:

@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;


    /**
     * 向rabbitTemplate 注入回调失败的类
     * 后置处理器:其他注解都执行结束才执行。
     */
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }
    /**
     * 交换机确认回调方法
     *  发消息 交换机接收到了  回调
     * @param correlationData :保存回调消息的 ID 及相关信息,交换机收到消息 ack=true代表成功;ack=false 代表失败
     * @param ack :true 代表交换机收到了
     * @param cause : 原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if(ack){
            log.info("交换机已经收到 ID 为:{} 的消息",correlationData.getId());
        }else{
//            rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
//                ConfirmConfig.CONFIRM_ROUTINGKEY,"message",correlationData);
            log.info("交换机未收到 ID为 {} 的消息,原因是 {}",correlationData.getId(),cause);
        }
    }

    /**
     * 当消息传送到队列过程中不可抵达的时候 将消息返回给生产者
     * @param message
     * @param replyCode
     * @param replyText
     * @param exchange
     * @param routingKey
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.error("消息 {} ,被交换机 {} 退回原因 {}",message,exchange,replyText);
    }
}

消费者

@Component
@Slf4j
public class WarningConsumer {
    // 以及创建好的 报警队列
    public static final String WARNING_QUEUE_NAME = "warning.queue";

    //接收报警消息
    @RabbitListener(queues = WARNING_QUEUE_NAME)  // 读取队列中的消息
    public void receiveWarningMsg(Message message){
        String msg = new String(message.getBody());
        log.info("报警发现不可路由消息:{}",msg);
    }
}

生产者

 // 测试备份交换机
    @GetMapping("/sendMessageBackups/{message}")
    public void sendMessageToBackupsExchange(@PathVariable String message){
        // 需要发送一个correlationData,此时交换机确认回调中携带了这个参数,相当于消息的标识
        CorrelationData correlationData = new CorrelationData("1");
       // 发送消息,生产者发送到交换机,交换机通过routing_key 发送到消费者 ,模拟错误,不能发送到指定队列
        rabbitTemplate.convertAndSend(BackupsConfig.BOOT_CFM_EXC,BackupsConfig.ROUTING_KEY+2,message,correlationData);
    }

测试结果

http://localhost:8080/ttl/sendMessageBackups/你好
image
可以看到,当消息不可抵达的时候,采用备份交换机,可以使得消息进行存储,通过备份队列进行消费。

总结

当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理。
注意事项:当退回模式和备份交换机一起使用的时候,备份交换机的优先级比较高,不会执行回退消息的回调。
(1)mandatory 参数告诉服务器至少将该消息路由到一个队列中,否则将消息返回给生产者。
(2)immediate 参数告诉服务器,如果该消息关联的队列上有消费者,则立刻投递;如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者,不用将消息存入队列而等待消费者了。
RabbitMQ 3.0 版本开始去掉了对immediate参数的支持,对此RabbitMQ官方解释是:immediate参数会影响镜像队列的性能,增加了代码复杂性,建议采用TTL和DLX的方法替代。

mandatory:参数在application.yml 文件中进行配置:

spring:
  rabbitmq:
    port: 5672
    host: xxxxx
    username: xxx
    password: xxx
    publisher-confirm-type: correlated #开启交换机确认回调
    publisher-returns: true # 开启队列回退消息
    listener:
      simple:
        acknowledge-mode: manual #开启手动确认
    template:
      mandatory: false  # 表示参数告诉服务器至少将该消息路由到一个队列中,否则将消息返回给生产者。

在 rabbitmqTemplate中调用convertAndSend 实际在底层调用的是channel.basicPublish方法。
image
image
上图是一个工作流程,实现

备份交换机 和 消息回退的区别:
消息回退(Message Requeue)和备份交换机(Alternate Exchange)都是RabbitMQ提供的保证消息不丢失的两种机制,主要区别如下:

  • 触发时机不同
    • 消息回退是在消费者端调用basicNack或basicReject触发
    • 备份交换机是在Exchange路由不到队列时触发
  • 处理方式不同
    • 消息回退是将消息重新入队,重新派发给消费者
    • 备份交换机是将消息路由到备份交换机绑定的队列
  • 使用场景不同
    • 消息回退适用于消费失败需要重试
    • 备份交换机适用于路由失败需要备份
  • 实现方式不同
    • 消息回退需要消费端实现消息ACK
    • 备份交换机需要额外声明备份交换机绑定队列
  • 性能不同
    • 消息回退可能导致消息重复消费
    • 备份交换机不会导致重复

综上,消息回退和备份交换机各有不同的使用场景,可以根据需求选择适合的机制,也可以同时使用两者。

posted @   自学Java笔记本  阅读(118)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示