RabbitMQ学习之发布确认SpringBoot版本

165 阅读4分钟

​ 本文已参与【新人创作礼】活动,一起开启掘金创作之路。

目录

一、使用场景

二、原理

确认机制方案

代码架构图

 三、代码实现

1.yml文件

2.配置类代码 

3.实现回调接口 

4.生产者代码

5.消费者代码

四、回退消息

1.Mandatory  参数

2.修改回调接口代码,其余不变

3.运行结果 

五、备份交换机

1.概念

2.使用范围

3.原理图

4.配置类文件

5.warning消费者 

 6.运行代码


一、使用场景

在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。

需要进行 RabbitMQ 的消息可靠投递,用到了发布确认模式。

二、原理

确认机制方案

​编辑

代码架构图

 ​编辑

 三、代码实现

1.yml文件

spring:
  rabbitmq:
    host: 192.168.16.106
    port: 5672
    username: guest
    password: guest
    publisher-confirm-type: correlated

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


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

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

2.配置类代码 

@Configuration
public class ConfirmConfig {
    //交换机
    public static final String CONFIRM_EXCHANGE_NAME = "confirm_exchange";
    //队列
    public static final String CONFIRM_QUEUE_NAME = "confirm_queue";
    //RoutingKey
    public static final String CONFIRM_ROUTING_KRY = "key1";

    @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 confirmQueueBindingExchange(@Qualifier("confirmQueue") Queue queue,
                                               @Qualifier("confirmExchange") DirectExchange directExchange){
        return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KRY);
    }
}

3.实现回调接口 

@Component //第一步
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback{

    @Autowired //第二步
    private RabbitTemplate rabbitTemplate;

    //注入
    @PostConstruct  //第三步
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1. 发消息  交换机接收到了 回调
     *   1.1 correlationData 保存回调的Id及相关信息
     *   1.2 交换机收到消息 ack=true
     *   1.3 cause null
     *
     * 2.发消息 交换机接收失败 回调
     *  2.1 correlationData保存回调的Id及相关信息
     *  2.2 交换机收到消息 ack=false
     *  2.3 cause 失败的原因
     * @param correlationData
     * @param ack
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if(ack){
            log.info(" 交换机已经收到 id  为:{} 的消息",correlationData.getId());
        }else{
            log.info(" 交换机还未收到 id  为:{} 消息, 由于原因:{}",correlationData.getId(),cause);
        }
    }

}

4.生产者代码

@RestController
@Slf4j
public class ProduceController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message){

        CorrelationData correlationData = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KRY,
                message+"1",correlationData);
        log.info("发送的消息:"+message+"1");

        CorrelationData correlationData2 = new CorrelationData("2");
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KRY+"2",message+"2",correlationData2);
        log.info("发送的消息:"+message+"2");
    }

}

5.消费者代码

@Component
@Slf4j
public class ConfirmConsumer {
    //接收消息
    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
    public void receive(Message message, Channel channel) throws Exception{
        String msg = new String(message.getBody());
        log.info("收到confirm.queue队列的消息:{}",msg);
    }
}

​编辑

可以看到,发送了两条消息,第一条消息的 RoutingKey 为 "key1",第二条消息的 RoutingKey 为
"key12",两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有第二条消息被直接丢弃了。 

四、回退消息

1.Mandatory  参数

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

2.修改回调接口代码,其余不变

@Component //第一步
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback{

    @Autowired //第二步
    private RabbitTemplate rabbitTemplate;

    //注入
    @PostConstruct  //第三步
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1. 发消息  交换机接收到了 回调
     *   1.1 correlationData 保存回调的Id及相关信息
     *   1.2 交换机收到消息 ack=true
     *   1.3 cause null
     *
     * 2.发消息 交换机接收失败 回调
     *  2.1 correlationData保存回调的Id及相关信息
     *  2.2 交换机收到消息 ack=false
     *  2.3 cause 失败的原因
     * @param correlationData
     * @param ack
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if(ack){
            log.info(" 交换机已经收到 id  为:{} 的消息",correlationData.getId());
        }else{
            log.info(" 交换机还未收到 id  为:{} 消息, 由于原因:{}",correlationData.getId(),cause);
        }
    }

    //可以在当消息传递过程中不可达目的地时将消息返回给生产者
    //只有不可达目的地的时候,才进行回退
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息{},被交换机{}退回,退回原因:{},路由key:{}",
                new String(message.getBody()),exchange,replyText,routingKey);
    }

}

3.运行结果 

​编辑

五、备份交换机

1.概念

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

2.使用范围

添加处理这些被退回的消息的逻辑,可以做到既不丢失消息,又不增加生产者的复杂性。

3.原理图

​编辑

4.配置类文件

@Configuration
public class ConfirmConfig {
    //交换机
    public static final String CONFIRM_EXCHANGE_NAME = "confirm_exchange";
    //队列
    public static final String CONFIRM_QUEUE_NAME = "confirm_queue";
    //RoutingKey
    public static final String CONFIRM_ROUTING_KRY = "key1";
    //备份交换机
    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("confirmExchange")
    public DirectExchange confirmExchange(){
        //添加备份交换机
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
                .withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
    }

    @Bean("backupExchange")
    public FanoutExchange backupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }
    @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
    public Binding confirmQueueBindingExchange(@Qualifier("confirmQueue") Queue queue,
                                               @Qualifier("confirmExchange") DirectExchange directExchange){
        return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KRY);
    }
    @Bean
    public Binding warningQueueBindingExchange(@Qualifier("backupQueue") Queue queue,
                                        @Qualifier("backupExchange") FanoutExchange fanoutExchange){
        return BindingBuilder.bind(queue).to(fanoutExchange);
    }

    @Bean
    public Binding backQueueBindingExchange(@Qualifier("warningQueue") Queue queue,
                                        @Qualifier("backupExchange") FanoutExchange fanoutExchange){
        return BindingBuilder.bind(queue).to(fanoutExchange);
    }
}

5.warning消费者 

@Component
@Slf4j
public class WarningConsumer {

    @RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
    public void receive(Message message){
        String msg = new String(message.getBody());
        log.info("报警发现不可路由消息:{}",msg);
    }
}

 6.运行代码

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

​编辑

​编辑

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