发布确认高级

1.概念

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

2.架构

3.实现

3.1 基础部分

  • 配置类
@Configuration
public class ConfirmConfig {

    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_ROUTING_key = "confirm.routingkey";

    //声明交换机
    @Bean
    public DirectExchange confirmExchange(){
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
               
    }

    //声明普通队列
    @Bean
    public Queue confirmQueue(){
        return new Queue(CONFIRM_QUEUE_NAME);
    }

    //绑定
    @Bean
    public Binding confirmQueueBindingconfirmExchange(@Qualifier("confirmQueue") Queue queue,
                                  @Qualifier("confirmExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(CONFIRM_ROUTING_key);
    }
}
  • 生产者
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ConfirmController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMsg/{msg}")
    public void sendMsg(@PathVariable String msg){
        CorrelationData correlationData = new CorrelationData("1");

        System.out.println(new Date() + msg);
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_key,
                msg, correlationData);

        CorrelationData correlationData1 = new CorrelationData("2");

        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_key+"01",
                msg, correlationData1);
    }

}
  • 消费者
@Slf4j
@Component
public class ConfirmConsumer {
    //接收消息
    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
    public void receiveD(Message message, Channel channel) throws Exception{
        String msg = new String(message.getBody());
        log.info("Confirm" + new Date() + msg);
    }
}

3.2 Exchange回调部分

  • application.yml
    NONE:禁用发布确认模式,是默认值
    CORRELATED:发布消息成功到交换器后会触发回调方法
    SIMPLE:
    经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,
    其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法
    等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑, 要注意的点是
    waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker
spring.rabbitmq.publisher-confirm-type= correlated
  • 回调方法
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    //注入当前实现方法
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1.发消息 交换机接收到了 回调
     * @param correlationData 保存回调信息的ID和相关信息
     * @param b 收到/没收到信息 ack = true/false
     * @param s 失败的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        String id = correlationData.getId();
        if(b){
         log.info("Success");
        } else {
            log.info("Failure");
        }
    }
}

3.3 queue回退部分

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

  • application.yml
spring.rabbitmq.publisher-returns= true
  • 回调类
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    //注入当前实现方法
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        String id = correlationData.getId();
        if(b){
         log.info("Success");
        } else {
            log.info("Failure");
        }
    }

    //可以在当消息传递过程中不可达目的地时将消息返回给生产者
    //只有不可到达时 才进行回退
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.info(returnedMessage.toString());
    }
}

4.交换机备份

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

4.1 实现

  • 配置类加入
    //备份交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup_exchange";
    public static final String BACK_QUEUE_NAME = "backup_queue";
    public static final String WARNING_QUEUE_NAME = "warning_queue";

 //声明交换机
    @Bean
    public DirectExchange confirmExchange(){
        Map<String, Object> arg = new HashMap<>();
        arg.put("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
                .withArguments(arg).build();
    }

    //备份交换机
    @Bean
    public FanoutExchange backupExchange(){

        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    @Bean
    public Queue backupQueue(){
        return new Queue(BACK_QUEUE_NAME);
    }

    @Bean
    public Queue warningQueue(){
        return new Queue(WARNING_QUEUE_NAME);
    }
  @Bean
    public Binding backQueueBindingbackupExchange(@Qualifier("backupQueue") Queue queue,
                                                      @Qualifier("backupExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    @Bean
    public Binding warningQueueBindingbackupExchange(@Qualifier("warningQueue") Queue queue,
                                                  @Qualifier("backupExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }
  • 消费者
@Slf4j
@Component
public class WarningConsumer {

    //接收消息
    @RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
    public void receiveD(Message message, Channel channel) throws Exception{
        String msg = new String(message.getBody());
        log.info("Warning" + new Date() + msg);
    }
}
posted @ 2023-06-20 12:41  lwx_R  阅读(2)  评论(0编辑  收藏  举报