Rabbitmq之发布确认高级、回退消息、备份交换机、幂等性、优先级队列、惰性队列

一、发布确认高级

1、简介

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRb0PHDf-1630999921196)(D:\学习资料\图片\image-20210903163221644.png)]

2、代码实现 

2.1、配置文件:添加

spring.rabbitmq.publisher-confirm-type = correlated

2.2、配置类

package com.kgf.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 配置类:发布确认(高级)
@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_key = "key1";

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

    @Bean
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    //绑定
    @Bean
    public Binding queueBindingExchange(@Qualifier("confirmQueue") Queue confirmQueue,
                                        @Qualifier("confirmExchange")DirectExchange confirmExchange){
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_routing_key);
    }
}

2.3、生产者

 // 开始发消息 测试确认
 @RestController
 @Slf4j
 @RequestMapping("/confirm")
 public class ProducerController {

 @Autowired
 private RabbitTemplate rabbitTemplate;

 //发消息
 @GetMapping("/sendMessage/{message}")
 public void sendMessage(@PathVariable String message){
     rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME
             ,ConfirmConfig.CONFIRM_routing_key
             ,message);
     log.info("发送消息内容:{}",message);
 }
}

2.4、消费者

 // 接收消息
 @Slf4j
 @Component
 public class Consumer {

 @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
 public void receiveConfirmMessage(Message message){
     String msg = new String(message.getBody());
     log.info("接受到的队列confirm.queue消息:{}",msg);
 }
}

2.5、回调接口

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

 @Autowired
 private RabbitTemplate rabbitTemplate;

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

 /*
 * 交换机确认回调方法,发消息后,交换机接收到了就回调
 *   1.1 correlationData:保存回调消息的ID及相关信息
 *   1.2 b:交换机收到消息,为true
 *   1.3 s:失败原因,成功为null
 *
 * 发消息,交换机接受失败,也回调
 *   2.1 correlationData:保存回调消息的ID及相关信息
 *   2.2 b:交换机没收到消息,为false
 *   2.3 s:失败的原因
 *
  * */

 @Override
 public void confirm(CorrelationData correlationData, boolean b, String s) {
     String id = correlationData!=null ? correlationData.getId():"";
     if (b){
         log.info("交换机已经收到ID为:{}的信息",id);
     }else {
         log.info("交换机还未收到ID为:{}的消息,由于原因:{}",id,s);
     }
   }
 }

3、配置文件及消息发送方

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

 @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_key
             ,message,correlationData);
     log.info("发送消息内容:{}",message);
   }
 }

但此时出了问题,如果交换器出现问题可以回调接口,但是如果队列出问题无法会掉借口

二、回退消息

1、简介

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

2、配置文件

spring.rabbitmq.publisher-returns=true

3、回退接口

package com.kgf.rabbitmq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

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

 @Autowired
 private RabbitTemplate rabbitTemplate;

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

 /*
 * 交换机确认回调方法,发消息后,交换机接收到了就回调
 *   1.1 correlationData:保存回调消息的ID及相关信息
 *   1.2 b:交换机收到消息,为true
 *   1.3 s:失败原因,成功为null
 *
 * 发消息,交换机接受失败,也回调
 *   2.1 correlationData:保存回调消息的ID及相关信息
 *   2.2 b:交换机没收到消息,为false
 *   2.3 s:失败的原因
 *
  * */

 @Override
 public void confirm(CorrelationData correlationData, boolean b, String s) {
     String id = correlationData!=null ? correlationData.getId():"";
     if (b){
         log.info("交换机已经收到ID为:{}的信息",id);
     }else {
         log.info("交换机还未收到ID为:{}的消息,由于原因:{}",id,s);
     }
   }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.error("消息{},被交换机{}退回,退回的原因:{},路由Key:{}",
                new String(message.getBody())
                ,exchange
                ,replyText
                ,routingKey
        );
    }
}

4、发布测试

 // 开始发消息 测试确认
 @RestController
 @Slf4j
 @RequestMapping("/confirm")
 public class ProducerController {

 @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_key
             ,message+"key1",correlationData);
     log.info("发送消息内容:{}",message+"key1");

     CorrelationData correlationData2 = new CorrelationData("2");
     rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME
             ,ConfirmConfig.CONFIRM_routing_key+"2"
             ,message+"key12",correlationData2);
     log.info("发送消息内容:{}",message+"key12");
 }
}

5、效果:实现回退消息成功!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y3D4XHoH-1630999921197)(D:\学习资料\图片\image-20210906141934613.png)]

三、备份交换机

1、简介

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i4qIy9FX-1630999921197)(D:\学习资料\图片\image-20210906145239686.png)]

2、配置类 

package com.kgf.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 配置类:发布确认(高级)
@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_key = "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
    public DirectExchange confirmExchange(){
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }

    @Bean
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    //绑定
    @Bean
    public Binding queueBindingExchange(@Qualifier("confirmQueue") Queue confirmQueue,
                                        @Qualifier("confirmExchange")DirectExchange confirmExchange){
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_routing_key);
    }

    //备份交换机
    @Bean
    public FanoutExchange backupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    //备份队列
    @Bean
    public Queue backupQueue(){
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }

    //报警队列
    @Bean
    public Queue warningQueue(){
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }

    @Bean
    public Binding backupQueueBindingBackupExchange(@Qualifier("backupQueue") Queue backupQueue,
                                                    @Qualifier("backupExchange") FanoutExchange backupExchange){
        return BindingBuilder.bind(backupQueue).to(backupExchange);
    }

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

3、消费者

  // 报警消费者
 @Component
 @Slf4j
 public class WarningConsumer {

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

4、效果

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

四、幂等性

1、简介

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

2、解决思路

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

  2. 在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.唯一ID+指纹码机制,利用数据库主键去重, b.利用redis.的原子性去实现

  3. 唯一ID+指纹码机制
    指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个id是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。

  4. Redis原子性
    利用redis执行setnx命令,天然具有幂等性,从而实现不重复消费

五、优先级队列

1、使用场景

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

2、代码实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ozfyhspj-1630999921198)(D:\学习资料\图片\image-20210906160545781.png)]

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

2.1、生产者

public class Producer {
    // 队列名称
    public static  final String QUEUE_NAME="hello";

    // 发消息
    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        // 工厂IP连接RabbitMQ的队列
        factory.setHost("192.168.163.128");
        // 用户名
        factory.setUsername("admin");
        // 密码
        factory.setPassword("123");

        // 创建连接
        Connection connection = factory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();
        
        Map<String, Object> arguments = new HashMap<>();
        //官方允许是0-255之间,此处设置10,允许优化级范围为0-10,不要设置过大,浪费CPU与内存
        arguments.put("x-max-priority",10);
        channel.queueDeclare(QUEUE_NAME,true,false,false,arguments);
        // 发消息
        for (int i = 0; i < 10; i++) {
            String message = "info" + i;
            if(i == 5){
                AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
                channel.basicPublish("",QUEUE_NAME,properties,message.getBytes(StandardCharsets.UTF_8));
            }else {
                channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));

            }
        }
        System.out.println("消息发送完毕!");
    }
}

2.2、消费者

public class Consumer {
    // 队列名称
    public static final String QUEUE_NAME = "hello";

    // 接受消息
    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("192.168.163.128");
        factory.setUsername("admin");
        factory.setPassword("123");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        // 声明 接受消息
        DeliverCallback deliverCallback = (consumerTag,message) -> {
            System.out.println(new String(message.getBody()));
        };
        // 声明 取消消息
        CancelCallback cancelCallback = consumer -> {
            System.out.println("消息消费被中断");
        };
        System.out.println("C2等待接受消息.......");
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

2.3、效果

六、惰性队列

1、使用场景 

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

2、两种模式

        队列具备两种模式: default和lazy.默认的为default模式,在3.6.0之前的版本无需做任何变更。lazy_模式即为惰性队列的模式,可以通过调用channel.queueDecare方法的时候在参数中设置,也可以通过
        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);

内存对比:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJrJA4le-1630999921198)(D:\学习资料\图片\image-20210906162624486.png)]

在发送1百万务消息,每条消息大概占1KB的情况下,普通队列占用内存是1.2GB,而惰性队列仅仅占用1.5MB 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值