Rabbitmq

RabbitMQ

RabbbitMq的作用:

解耦:

  • image-20220223235818997

削峰:

  • 基于队列的特性,讲消息压在队列中,保证服务器不会一次性接收到过多请求

异步:

  • 会开一个线程来处理服务

RabbitMQ工作模型:

image-20220224001602688

交换机:

1.Direct Exchange直连
  • 消息通过routing key发送到交换机,交换机根据routing key匹配binding key路由到指定队列。
  • 一对一

image-20220224002630609

2.Topic Exchange主题
  • 消息通过routing key发送到交换机,交换机根据routing key的通配符匹配binding key路由到指定队列。
  • 一对多

image-20220224003333031

3.Fanout Exchange广播
  • 将消息发送到指定交换机下面所有队列
  • 一对所有

image-20220224003743333

死信队列:

概述:
  • 顾名思义,就是无法被消费的消息,字面意思可以这样理解,一般来说,producter将消息投递到broker或者直接到queue里了,consumer从queue取出消息进行消费,但由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没用后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:
  • 为了保证订单业务的消息数据不丢失,需要使用到RabbitMq的死信队列机制,当消息发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击去支付在指定时间未支付时自动失效。
来源:
  • 消息TTL过期
  • 队列达到最大长度(队列满了,无法再添加到数据到mq中)
  • 消息被拒绝(basic.reject或basic.nack)并且requeue=false

工作模型:

  • image-20220224191600040

1.消息TTL过期

	/**
     * 普通队列
     * @return
     */
    @Bean
    public Queue queue(){
        //绑定死信交换机
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", RabbitMqUtils.DEAD_EXCHANGE_KEY);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "dead.key");
        //延时队列的使用
        //设置ttl过期时间,如果指定时间内,没有被消费,就会被放入死信队列中
        args.put("x-message-ttl", 10000);
//        return new Queue(RabbitMqUtils.DIRECT1_QUEUE_KEY);
        return QueueBuilder.durable(RabbitMqUtils.DIRECT1_QUEUE_KEY).withArguments(args).build();
    }

2.队列达到最大长度

//设置正常队列长度限制
        args.put("x-max-length",5);

3.消息被拒绝

//拒绝单个消息,重新写入队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
//应答单个消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);

延迟队列:

概念:

  • 延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望再指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

使用场景:

  • 订单在十分钟之内未支付则自动取消
  • 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒
  • 用户注册成功后,如果三天内没用登录则进行短信提醒
  • 用户发起退款,如果三天内没用得到处理则通知相关运营人员。
  • 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

设置:

前提就是都需要绑定死信交换机

  1. 设置队列中所有消息都是TTL

    • @Bean
          public Queue queue(){
              //绑定死信交换机
              Map<String, Object> args = new HashMap<>(3);
              //声明当前队列绑定的死信交换机
              args.put("x-dead-letter-exchange", RabbitMqUtils.DEAD_EXCHANGE_KEY);
              //声明当前队列的死信路由 key
              args.put("x-dead-letter-routing-key", "dead.key");
              //延时队列的使用
              //设置ttl过期时间,如果指定时间内,没有被消费,就会被放入死信队列中
              args.put("x-message-ttl", 10000);
              //设置正常队列长度限制
              args.put("x-max-length",5);
      //        return new Queue(RabbitMqUtils.DIRECT1_QUEUE_KEY);
              return QueueBuilder.durable(RabbitMqUtils.DIRECT1_QUEUE_KEY).withArguments(args).build();
          }
      
  2. 设置单条消息的TTL

    • @GetMapping("/send2")
      public void send2(){
          Mq mq = new Mq();
          mq.setMessageId(UUID.randomUUID().toString());
          mq.setCount(0);
          mq.setStatus(0);
          mq.setMessageContent("测试ttl");
          MessageProperties messageProperties = new MessageProperties();
          messageProperties.setMessageId(mq.getMessageId());
          messageProperties.setContentType("text/plain");
          messageProperties.setContentEncoding("utf-8");
          Message message = new Message("hello,message idempotent!".getBytes(), messageProperties);
          //将消息存入数据库中
          mqService.save(mq);
          rabbitmq.convertAndSend(
                  RabbitMqUtils.DIRECT_EXCHANGE_KEY,
                  "queue2.exchange",
                  message,c->{
                      MessageProperties messageProperties1 = c.getMessageProperties();
                      //设置时间为20秒过期
                      messageProperties.setExpiration("20000");
                      return c;
                  }
          );
      }
      

两者的区别:

​ 如果设置了队列的TTL属性,那么消息一旦过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意一点,如果不设置TTL,表示消息永远不会过期,如果将TTl设置为0,则表示此时可以直接投递该消息到消费者,否则该消息将会被丢弃。

单条消息TTL虽然好是好但是还有问题:

​ 如果发送两条消息 ,第一条消息延时时间为10分钟,第二条消息为2分钟,那么RabbitMq只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

解决方案:

  • 安装延时插件
    • 在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载
      rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。
      进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ
      /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
      rabbitmq-plugins enable rabbitmq_delayed_message_exchange

image-20220224203000179

重新定义要给延时交换机,测试就会发现,第二条消息会被先执行。

备份队列:

概述:

  • 消息回退与消息确认的升级版。
  • 如果交换机路由到队列的过程中出现了问题,既没有消息确认,或根据路由key找不到队列,此时消息就会到备份交换机中。

代码实现:

  • 需要定义一个备份交换机,普通交换机,一个警告队列,一个备份队列,一个普通队列
  • 备份交换机一般设置为扇出类型,也就是广播类型
/**
 * 警告队列
 * @return
 */
@Bean
public Queue warningQueue(){
    return new Queue(RabbitMqUtils.WARNING_QUEUE_KEY);
}

/**
 * 备份队列
 * @return
 */
@Bean
public Queue backupQueue(){
    return new Queue(RabbitMqUtils.BACK_UP_QUEUE_KEY);
}
/**
 * 备份交换机
 * @return
 */
@Bean
public FanoutExchange backupExchange(){
    return new FanoutExchange(RabbitMqUtils.BACK_UP_EXCHANGE_KEY);
}
/**
 * 警告队列与备份交换机
 */
@Bean
public Binding warningqueueBindExchange(
        @Qualifier("warningQueue") Queue queue,
        @Qualifier("backupExchange")FanoutExchange fanoutExchange
){
    return BindingBuilder.bind(queue).to(fanoutExchange);
}

/**
 * 备份队列与备份交换机绑定
 * @param queue
 * @param fanoutExchange
 * @return
 */
@Bean
public Binding backupqueueBindExchange(
        @Qualifier("backupQueue") Queue queue,
        @Qualifier("backupExchange")FanoutExchange fanoutExchange
){
    return BindingBuilder.bind(queue).to(fanoutExchange);
}

/**
 * 普通交换机与备份交换机
 */
@Bean
public DirectExchange directExchange(){
    return ExchangeBuilder
            .directExchange(RabbitMqUtils.DIRECT_EXCHANGE_KEY)
            .durable(true)
            // 绑定备份交换机
            .alternate(RabbitMqUtils.BACK_UP_EXCHANGE_KEY)
            .build();
}

发送消息的时候指定一个不存在的路由key,进行测试

//监听警告队列
    @RabbitListener(queues = RabbitMqUtils.WARNING_QUEUE_KEY)
    public void directWARNINGqueue(Message message, Channel channel) throws IOException {
//        channel.basicConsume(RabbitMqUtils.DIRECT1_QUEUE_KEY,true,message);

        String msg = new String(message.getBody());
        log.info("当前时间:{},收到警告队列信息{}",new Date(),msg);
    }
    //监听备份队列
    @RabbitListener(queues = RabbitMqUtils.BACK_UP_QUEUE_KEY)
    public void directBACKqueue(Message message){
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到备份队列信息{}",new Date(),msg);
        channel.basicPublish("",RabbitMqUtils.DIRECT1_QUEUE_KEY,null,msg.getBytes());
        log.info("消息被重新投递到队列中!");
    }
//向普通队列发送消息
rabbitTemplate.convertAndSend(
        RabbitMqUtils.DIRECT_EXCHANGE_KEY,
        "queue.exchangelll",
        "message1"
);

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

优先级队列:

1.在web页面添加

image-20220224204936072

2.在队列中添加

Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);

3.在消息发送的时候添加

Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
AMQP.BasicProperties properties = new
AMQP.BasicProperties().builder().priority(5).build();

4.注意

要让队列实现优先级需要做的事情有如下事情:

  • 队列需要设置为优先级队列
  • 消息需要设置消息的优先级
  • 消费者需要等待消息已经发送到队列中去消费
  • 因为这样才有机会对消息进行排序

惰性队列:

概述:

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

两种模式:

队列具备两种模式: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);

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

面试题:

1.为什么用MQ?为什么要用RabbitMQ?

  • MQ是用来解决通信问题,解耦服务与服务之间的关系

2.如果消息发送到客户端,消息在消费的过程出错了,怎么解决?

  • 使用消息手动确认模式,当消息被成功消费了,确认应答,如果消息消费的过程中失败了,拒绝应答,将消息放入死信队列,重试.

写一个综合案例:

  • 消息发到交换机失败提示

    • 在rabbitmq配置文件中配置

    • // 配置rabbitmq的消息转化器,默认是java的序列化,可以设置成json
      //        rabbitTemplate.setMessageConverter(converter());
              //消息是否成功发送到Exchange
              rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
                  if (ack){
                      log.info("消息发送到Exchange成功{},cause{}",correlationData,cause);
                  }else{
                      log.error("消息发送到Exchange失败{},cause{}",correlationData,cause);
                  }
              });
      
  • 消息发送到交换机,没有路由的队列放入备份交换机中

    • 配置备份交换机

    • /**
       * 普通交换机与备份交换机
       */
      @Bean
      public DirectExchange directExchange(){
          return ExchangeBuilder
                  .directExchange(RabbitMqUtils.DIRECT_EXCHANGE_KEY)
                  .durable(true)
                  // 绑定备份交换机
                  .alternate(RabbitMqUtils.BACK_UP_EXCHANGE_KEY)
                  .build();
      }
      
  • 消息发送成功之后消费者因为业务失败导致消息消费失败,放入死信队列中,重试

    • 配置死信交换机

    • /**
           * 普通队列
           *      绑定了死信交换机
           *      设置列队列中TTL消息指定时间内没有被消费,就放入死信队列中
           *      设置了队列长度限制
           * @return
           */
          @Bean
          public Queue queue(){
              //绑定死信交换机
              Map<String, Object> args = new HashMap<>(3);
              //声明当前队列绑定的死信交换机
              args.put("x-dead-letter-exchange", RabbitMqUtils.DEAD_EXCHANGE_KEY);
              //声明当前队列的死信路由 key
              args.put("x-dead-letter-routing-key", "dead.key");
              //延时队列的使用
              //设置ttl过期时间,如果指定时间内,没有被消费,就会被放入死信队列中
              args.put("x-message-ttl", 10000);
              //设置正常队列长度限制
              args.put("x-max-length",5);
      //        return new Queue(RabbitMqUtils.DIRECT1_QUEUE_KEY);
              return QueueBuilder.durable(RabbitMqUtils.DIRECT1_QUEUE_KEY).withArguments(args).build();
          }
      
  • 消息应答,消息确认,高级消息确认,消息入库

    • 开启消息确认模式

    •   rabbitmq:
          # 确保消息成功发送到交换机
          publisher-confirm-type: correlated
          # 确保消息在未被队列接收时返回
          publisher-returns: true
          listener:
            simple:
              # 指定消息没有被队列接收是是否强行退回还是直接丢弃
      #        acknowledge-mode: manual
              # 指定一个请求能处理多少个消息
              prefetch: 100
      
    • 配置setMandatory

    • //开启确认应答
      rabbitTemplate.setMandatory(false);
      //消息从Exchange路由到Queue失败回调.
      rabbitTemplate.setReturnsCallback((returnCallback)->{
              log.error("消息从Exchange路由到Queue失败:exchange:{},route:{},replyCode:{},replyText:{},message:{}",
                      returnCallback.getExchange(),
                      returnCallback.getRoutingKey(),
                      returnCallback.getReplyCode(),
                      returnCallback.getReplyText(),
                      returnCallback.getMessage()
              );
      });
      
    • 手动消息确认与拒绝

    • /**
                   * 消息确认:
                   *      参数一:消息标签
                   *      参数二:true 以确认所有消息,包括提供的交付标签; false 仅确认提供的交付标签。
                   */
      channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
      /**
                   * 消息拒绝:(在业务出异常的时候调用)
                   *      参数一:消息标签
                   *      参数二:true 以确认所有消息,包括提供的交付标签; false 仅确认提供的交付标签。
                   *      参数三:消息是丢弃还是从新排队,丢弃后会直接进入死信队列中
                   */
      channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
      
      
    • 当消息确认与备份交换机同时设置了,优先使用备份交换机配置

  • 延迟队列,惰性队列

    • 延时队列:
    • 可以设置队列里的消息在多长时间之后执行。
    • 惰性队列:
    • 将消息加载进磁盘,在被消费的时候才会加载进内存。

本文中所有代码在码云上,欢迎各位大佬下载!

https://gitee.com/hemingcong/hhh

posted @   疯自*  阅读(79)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示