RabbitMQ(七)高级发布确认与优先级队列

RabbitMQ(七)高级分布确认与优先级队列


8 发布确认高级

​ 在生产环境中由于一些不明原因,而导致rabbitMQ重启,在RabbitMQ重启阶段生产者消息投递失败,导致消息丢失,需要手动处理和恢复,这种情况可以使用确认机制。

8.1 交换机确认方案

8.1.1 确认机制方案

在这里插入图片描述

8.1.2 代码架构图

在这里插入图片描述

8.1.3 配置文件

​ 需要在配置文件中添加

spring:
  rabbitmq:
    # 发布消息成功到交换机后会触发回调方法
    publisher-confirm-type: correlated
  • NONE

    禁用发布确认模式,默认

  • CORRELATED

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

  • SIMPLE

    经过测试有两种效果,其一效果和CORRELATED一样会调用回调方法,

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

8.1.4 实战代码

  1. 结构配置类代码

    /**
     * 确认高级
     */
    @Configuration
    public class ConfirmQueueConfig {
          
          
    
      //交换机名称
      public static final String CONFIRM_EXCHANGE = "confirm.exchange";
      public static final String CONFIRM_QUEUE = "confirm.queue";
      //routingKey
      public static final String CONFIRM_ROUTING_KEY = "key1";
    
    
    
      @Bean("confirmExchange")
      public DirectExchange confirmExchange() {
          
          
        return new DirectExchange(CONFIRM_EXCHANGE);
    
      }
    
      @Bean("confirmQueue")
      public Queue confirmQueue() {
          
          
        return QueueBuilder.durable(CONFIRM_QUEUE).build();
      }
    
      @Bean
      public Binding confirmQueueBindingConfirmExchange(@Qualifier("confirmQueue") Queue queue,
                                                        @Qualifier("confirmExchange") DirectExchange directExchange) {
          
          
        return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KEY);
      }
    }
    
    
  2. 消息生产者代码

@Slf4j
@Api(tags = "高级确认")
@RestController
@RequestMapping("confirm")
public class ConfirmController {
    
    

  @Resource
  RabbitTemplate rabbitTemplate;

  @ApiOperation(value = "高级确认")
  @GetMapping("sendMsg/{message}")
  public boolean sendMsg(@PathVariable String message) {
    
    
    CorrelationData correlationData = new CorrelationData("1");
    //   发送正确消息
    rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE, ConfirmQueueConfig.CONFIRM_ROUTING_KEY,
                                  message, correlationData);
    log.info("接口发出消息{}", message);
    //   发送错误key消息
    CorrelationData correlationData2 = new CorrelationData("2");
    rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE+ "12", ConfirmQueueConfig.CONFIRM_ROUTING_KEY ,
                                  message, correlationData2);
    log.info("接口发出消息{}", message);

    return true;
  }
}

  1. 回调接口

    /**
     * 确认实现回调
     */
    @Slf4j
    @Component
    public class MyCallBack implements RabbitTemplate.ConfirmCallback {
          
          
    
      @Autowired
      private RabbitTemplate rabbitTemplate;
    
      //当前接口注入到RabbitTemplate中
      @PostConstruct
      public void init() {
          
          
        //注入
        rabbitTemplate.setConfirmCallback(this);
      }
    
      /**
         * 交换机确认回调方法
         * 1. 发消息 交换机接收到(成功回调)
         * 1.1 correlationData 保存回调信息的ID以及相关信息
         * 1.2 交换机收到消息 ack = true
         * 1.3 cause null 因为成功 所以失败原因为空
         * <p>
         * 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) {
          
          
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
          
          
          log.info("交换机已经收到id为:{}的消息", id);
        } else {
          
          
          log.info("交换机还未收到id为:{}的消息,由于原因:{}", id, cause);
        }
    
      }
    
    }
    
    
  2. 消息消费者

    @Slf4j
    @Component
    public class Consumer {
          
          
    
    @RabbitListener(queues = ConfirmQueueConfig.CONFIRM_QUEUE)
    public void receiveConfirmQueue(Message message) throws UnsupportedEncodingException {
          
          
    String msg = new String(message.getBody(), "UTF-8");
    log.info("当前时间: {}.收到队列的消息: {}", new Date(), msg);
    }
    
    }
    

8.1.5 结果分析

发起请求

http://127.0.0.1:9191/confirm/sendMsg/123

在这里插入图片描述

​ 从图片中可以看到,当请求了这个接口之后,会发送两条消息,由于第一条消息里面的信息都是正确的,所以他调用了正确的回调方法,而第二条的消息的交换机名称是错误的,所以他就调用了错误的回调方法,并打印了错误的原因。

8.2 消息确认方案(回退消息)

8.2.1 Mandatory参数

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可以路由,那么消息就会被直接丢弃,此时生产者是不知道消息被丢弃这个事情的。所以可以通过设置mandatory参数在消息传递过程中不可达目的地时将消息返回给生产者。

在这里插入图片描述

8.2.2 配置文件

添加回退消息配置

spring:
  rabbitmq:
    # 发送消息给信道,如果没有接收则回退消息
    publisher-returns: true

8.2.3 实战代码

  1. 生产者代码

    @ApiOperation(value = "高级确认")
    @GetMapping("sendMsg/{message}")
    public boolean sendMsg(@PathVariable String message) {
          
          
    CorrelationData correlationData = new CorrelationData("1");
    //   发送正确消息
    rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE, ConfirmQueueConfig.CONFIRM_ROUTING_KEY,
    message, correlationData);
    log.info("接口发出消息{}", message);
    //   发送错误key消息
    CorrelationData correlationData2 = new CorrelationData("2");
    rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE, ConfirmQueueConfig.CONFIRM_ROUTING_KEY + "12",
    message + "(错误交换机名称)", correlationData2);
    log.info("接口发出消息{}", message + "(错误交换机名称)");
    
    return true;
    }
    
    
    
  2. 回调方法

    /**
     * 确认实现回调
     */
    @Slf4j
    @Component
    public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
          
          
      @Autowired
      private RabbitTemplate rabbitTemplate;
    
      //当前接口注入到RabbitTemplate中
      @PostConstruct
      public void init() {
          
          
        //注入
        rabbitTemplate.setConfirmCallback(this);
    
        //注入
        rabbitTemplate.setReturnsCallback(this);
      }
    
      @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);
        }
    
      }
    
      @SneakyThrows
      @Override
      public void returnedMessage(ReturnedMessage returnedMessage) {
          
          
        System.out.println(returnedMessage);
        log.error("消息{},被交换机{}退回,退回原因:{},路由Key:{}",
                  new String(returnedMessage.getMessage().getBody(), "UTF-8"),
                  returnedMessage.getExchange(), returnedMessage.getReplyText(), returnedMessage.getRoutingKey());
      }
    }
    
    

8.2.4 结果分析

发起请求

http://127.0.0.1:9191/confirm/sendMsg/123

在这里插入图片描述

​ 从图片中可以看到,当消息被接收,我们可以在回调中知道是哪一条数据没有接收到,可以对没有接收到的数据进行保存重发等操作。

8.3 备份交换机

​ 对于无法投递的消息,我们会希望有一个备份交换机,当交换机接收到一条不可路由的消息时,会将这条消息转发到备份交换机中,由备份交换机进行转发和处理,通常备份交换机的类型为Fanout,这样就能把这条消息投递到与其绑定的所有队列,当然我们还可以建立一个报警队列,通独立的消费者来进行监测和报警。

8.3.1 代码架构图

在这里插入图片描述

8.3.2 实战代码

  1. 修改配置类

    @Configuration
    public class ConfirmQueueConfig {
          
          
    
      //交换机名称
      public static final String CONFIRM_EXCHANGE = "confirm.exchange";
      public static final String CONFIRM_QUEUE = "confirm.queue";
      //routingKey
      public static final String CONFIRM_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 = "waring.queue";
    
      @Bean("confirmExchange")
      public DirectExchange confirmExchange() {
          
          
    
        HashMap<String, Object> maps = new HashMap<>();
        //指定备份交换机
        maps.put("alternate-exchange", BACKUP_EXCHANGE);
    
        //关联备份交换机
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE).durable(true)
          .withArguments(maps).build();
      }
    
      @Bean("confirmQueue")
      public Queue confirmQueue() {
          
          
        return QueueBuilder.durable(CONFIRM_QUEUE).build();
      }
    
      //备份交换机
      @Bean("backupExchange")
      public FanoutExchange backupExchange() {
          
          
        return new FanoutExchange(BACKUP_EXCHANGE);
      }
    
      @Bean("warningQueue")
      public Queue warningQueue() {
          
          
        return QueueBuilder.durable(WARNING_QUEUE).build();
      }
    
      @Bean("backupQueue")
      public Queue backupQueue() {
          
          
        return QueueBuilder.durable(BACKUP_QUEUE).build();
      }
    
    
      @Bean
      public Binding confirmQueueBindingConfirmExchange(@Qualifier("confirmQueue") Queue queue,
                                                        @Qualifier("confirmExchange") DirectExchange directExchange) {
          
          
        return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KEY);
      }
    
      //备份队列绑定备份交换机
      @Bean
      public Binding backupQueueBindingConfirmExchange(@Qualifier("backupQueue") Queue queue,
                                                       @Qualifier("backupExchange") FanoutExchange backupExchange) {
          
          
        return BindingBuilder.bind(queue).to(backupExchange);
      }
    
      //报警队列绑定备份交换机
      @Bean
      public Binding waringQueueBindingConfirmExchange(@Qualifier("warningQueue") Queue queue,
                                                       @Qualifier("backupExchange") FanoutExchange backupExchange) {
          
          
        return BindingBuilder.bind(queue).to(backupExchange);
      }
    }
    
    
  2. 报警消费者

    /**
     * 报警消费者
     */
    @Slf4j
    @Component
    public class WaringConsumer {
          
          
    
      //接收报警消息
      @RabbitListener(queues = ConfirmQueueConfig.WARNING_QUEUE)
      public void receiveWaringMsg(Message message) {
          
          
        String msg = new String(message.getBody());
        log.error("报警发现不可路由消息:{}", msg);
      }
    }
    

8.3.3 结果分析

发起请求

http://127.0.0.1:9191/confirm/sendMsg/123

在这里插入图片描述

​ 通过这个例子一个是可以看出备份交换机的作用,还有一个是可以看出mandatory参数与备份交换机一起使用的时候,备份交换机的优先级更高。

9 RabbitMQ的其他知识点

9.1 幂等性

9.1.1 概念

​ 用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。 举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。

9.1.2 消息重复消费

​ 消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

9.1.3 解决思路

​ MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费 者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。

9.1.4 消费端幂等性保障

​ 在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:

​ a. 唯一 ID+指纹码机制,利用数据库主键去重,。

​ b.利用 redis 的原子性去实现。

​ 利用redis执行setnx命令,天然具有幂等性。从而实现不重复消费

9.2 优先级队列

9.2.1 使用场景

​ 在我们系统中有一个订单催付的场景,我们的客户在软件下的订单,软件会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,商家对我们来说,肯定是要分大客户和小客户的对吧,比如像大品牌的大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级, 否则就是默认优先级。

9.2.2 代码中如何添加

// 队列中添加优先级参数
HashMap<String, Object> maps = new HashMap<>();
//官方允许的值为0-255,此处设置为10,允许优先级范围为0-10,不要设置过大,浪费cpu与内存
maps.put("x-max-priority", 10);

channel.queueDeclare(QueueName.Priority_Queue.getName(), true, false, false, maps);

// 消息中添加优先级 priority 后面就是优先级参数,越大越优先
AMQP.BasicProperties properties =
  new AMQP.BasicProperties().builder().priority(i).build();
channel.basicPublish("", QueueName.Priority_Queue.getName(), properties, (message + i).getBytes());

​ 注意:要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序

9.2.3 实战代码

  1. 消息生产者

    /**
     * 优先级队列
     */
    public class Producer {
          
          
    
      public static void main(String[] args) throws Exception {
          
          
        //创建连接
        Connection connection = RabbitMqUtil.getConnection();
    
        //获取信道
        Channel channel = connection.createChannel();
        HashMap<String, Object> maps = new HashMap<>();
        //官方允许的值为0-255,此处设置为10,允许优先级范围为0-10,不要设置过大,浪费cpu与内存
        maps.put("x-max-priority", 10);
        
    channel.queueDeclare(QueueName.Priority_Queue.getName(), true, false, false, maps);
        String message = "hello word";
    
        for (int i = 0; i < 10; i++) {
          
          
    
          switch (i) {
          
          
            case 8:
            case 5:
            case 3: {
          
          
              AMQP.BasicProperties properties =
                new AMQP.BasicProperties().builder().priority(i).build();
              channel.basicPublish("", QueueName.Priority_Queue.getName(), properties, (message + i).getBytes());
              System.out.println("消息已经发送");
            }
              break;
            default: {
          
          
              AMQP.BasicProperties properties =
                new AMQP.BasicProperties().builder().priority(0).build();
              channel.basicPublish("", QueueName.Priority_Queue.getName(), null, (message + i).getBytes());
              System.out.println("消息已经发送");
            }
    
          }
    
    
        }
      }
    }
    
    
  2. 消费者代码

    /**
     * 消费者
     */
    public class Consumer {
          
          
    
      public static void main(String[] args) throws Exception {
          
          
        Connection connection = RabbitMqUtil.getConnection();
        Channel channel = connection.createChannel();
    
        // 声明接收消息
        DeliverCallback deliverCallback = (consumerTag, message) -> {
          
          
          System.out.println(new String(message.getBody()));
        };
    
        //取消消息回调
        CancelCallback cancelCallback = consumerTag -> {
          
          
          System.out.println("消息被中断");
        };
    
    channel.basicConsume(QueueName.Priority_Queue.getName(), true, deliverCallback, cancelCallback);
      }
    }
    
    

9.2.4 结果分析

​ 开启生产者代码,可以在UI中看到

在这里插入图片描述

打开消费者

在这里插入图片描述

可以看到消息按照优先级顺序排列消费。

9.3 惰性队列

9.3.1 使用场景

​ RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
​ 默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中, 这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。

9.3.2 两种模式

​ 队列具备两种模式:default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。 如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
​ 在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。下面示例中演示了一个惰性队列的声明细节:

Map<String, Object> args = new HashMap<String, Object>(); 
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);

9.3.3 内存开销对比

在这里插入图片描述

​ 在发送一百万条消息,每条消息大概占用1KB的情况下,普通队列占用内存是1.2GB,而惰性队列仅仅占用1.5M(因为惰性队列内存只存储消息的id,消息具体是存放在磁盘上的)。


                                                             (RabbitMQ 完)
                                                               2022.08.05
                                                         恭喜小华喜提新车车牌

(内心)
请添加图片描述

猜你喜欢

转载自blog.csdn.net/qq_27331467/article/details/126180169