RabbitMQ从理论——实战——面试题

一、RabbitMQ的工作模式

1、工作队列模式

就是生产者生产消息放到MQ队列中,有几个消费者去轮询的消费队列里的消息

在这里插入图片描述

2、发布订阅模式

在开发的时候有一些消息需要不同的消费者做不同的处理,也就是需要发送给不同的接口去进行业务逻辑处理。场景:电商网站的同一条促销消息需要发送短信、邮件发送、站内发送等等,此时就可以使用发布订阅模式
原理:就是不把消息发送到队列里面,将消息发送到交换机由交换机绑定多个队列再发送到队列里面
这样每个队列里面就都有交换机发出的消息

在这里插入图片描述

3、路由规则模式

根据特定的路由规则去对应的发消息(我虽然交换机都绑定了这些队列但是我可以根据我路由的规则去决定往哪里面发消息)
场景:电商网站中可能有一些重要的活动需要发邮件和短信通知,普通活动只发短信通知
原理:通过交换机和路由关键字来实现,队列不单单绑定交换机还加上个路由,发送者在发送的时候指定发送到哪个交换机并且指定路由键,这样就实现了通过交换机发送到指定的队列。

在这里插入图片描述

二、SpringBoot整合RabbitMQ

接收方主要就是一直去监听信息,发送方才是关键

1、编写生产者 config核心配置类

在这里插入图片描述

2、配置文件

在这里插入图片描述

3、发送消息类

在这里插入图片描述

4、编写消费者

消费者简单 只需要一直监听队列就可以了

在这里插入图片描述

三、消息的可靠性传递问题

须知:首先在RabbitMQ中 创建的交换机 和 队列 都可以设置持久化到硬盘中,所以这两块不会丢失。那么消息会在哪里丢失呢 无非就是发送过程中消息丢了

RabbitMQ消息投递的路径为:
生产者———>交换机———>队列———>消费者

为什么会在传递途中丢失呢:可能你的交换机名字写错了 队列名写错了,消息发送到一个没有的交换机或队列里,就发没了但是你也不知道,因为发送方发出去就不管了

在RabbitMQ工作的过程中,每个环节消息都可能传递失败,那么RabbitMQ是如何监听消息是否成功投递的呢?
确认模式(confirm)可以监听消息是否从生产者成功传递到交换机。
退回模式(return)可以监听消息是否从交换机成功传递到队列。
消费者消息确认(Consumer Ack)(就是手动签收)可以监听消费者是否成功处理消息。

1、解决生产者消息是否成功传递到交换机

确认模式(confirm)(注意:不管消息发送成功与否 都会调用这个回调方法)

生产者配置文件开启这个confirm模式

spring:
  rabbitmq:
  # 开启确认模式
   publisher-confirm-type: correlated

具体代码 在代码中定义回调方法 重写这个confirm回调方法 在这里面做处理
注:当你消息发出去 由于交换机名字写错了可能就导致你的消息丢失没有发送到已有的交换机中

回调方法里面参数有个boolean的ack参数,通过它来判断消息是否发送到交换机里,可以做一些业务的处理 比如说如果失败调用企业微信机器人发消息进行通知开发人员,或者记录日志记录发送失败的原因

@SpringBootTest
public class ProducerTest {
  @Autowired
  private RabbitTemplate rabbitTemplate;


  @Test
  public void testConfirm(){
    // 定义确认模式的回调方法,消息向交换机发送后会调用confirm方法
    rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
      /**
       * 被调用的回调方法
       * @param correlationData 相关配置信息
       * @param ack 交换机是否成功收到了消息
       * @param cause 失败原因
       */
      @Override
      public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack){
          System.out.println("confirm接受成功!");
         }else{
          System.out.println("confirm接受失败,原因为:"+cause);
          // 做一些处理。
         }
       }
     });
    rabbitTemplate.convertAndSend("my_topic_exchange","my_routing","send message...");
   }
}

2、解决交换机是否将消息成功传递到队列

退回模式(return)(注意:只有消息发送失败后才会调用这个回调方法 在里面写上自己的逻辑)

生产者配置文件开启退回模式

spring:
rabbitmq:
# 开启确认模式
publisher-confirm-type: correlated
# 开启回退模式
publisher-returns: true

生产者定义退回模式的回调方法,只有消息发送失败了才会调用此方法
注:当你消息成功发送到交换机,但是交换机发送给队列时由于路由关键字写错了可能就导致消息丢失

@SpringBootTest
public class ProducerTest {
  @Autowired
  private RabbitTemplate rabbitTemplate;


  @Test
  public void testReturn(){
    // 定义退回模式的回调方法。交换机发送到队列失败后才会执行returnedMessage方法
    rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
      /**
       * @param returned 失败后将失败信息封装到参数中
       */
      @Override
      public void returnedMessage(ReturnedMessage returned) {
        System.out.println("消息对象:"+returned.getMessage());
        System.out.println("错误码:"+returned.getReplyCode());
        System.out.println("错误信息:"+returned.getReplyText());
        System.out.println("交换机:"+returned.getExchange());
        System.out.println("路由键:"+returned.getRoutingKey());
        // 处理消息...
       }
     });
    rabbitTemplate.convertAndSend("my_topic_exchange","my_routing1","send message...");
   }
}

3、重点:解决消费者是否成功接收到消息

ACK手动签收消息确认

消费者配置开启手动签收

spring:
  rabbitmq:
   host: 192.168.0.162
   port: 5672
   username: itbaizhan
   password: itbaizhan
   virtual-host: /
  # 开启手动签收  自动签收为none
   listener:
    simple:
     acknowledge-mode: manual

消费者处理消息时定义手动签收和拒绝签收的情况
获取消息的投递序号 在业务代码就行trycatch,当代码无异常走到最后的时候通过信道channel进行手动签收消息 消费掉消息,当出现异常进入catch里面后通过信道channel拒签消息,消息重新放回到队列里面

@Component
public class AckConsumer {
  @RabbitListener(queues = "my_queue")
  public void listenMessage(Message message, Channel channel) throws IOException, InterruptedException {
    // 消息投递序号,消息每次投递该值都会+1
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
      int i = 1/0; //模拟处理消息出现bug
      System.out.println("成功接受到消息:"+message);
      // 签收消息
      /**
       * 参数1:消息投递序号
       * 参数2:是否一次可以签收多条消息
       */
      channel.basicAck(deliveryTag,true);
     }catch (Exception e){
      System.out.println("消息消费失败!");
      Thread.sleep(2000);
      // 拒签消息
      /**
       * 参数1:消息投递序号
       * 参数2:是否一次可以拒签多条消息
       * 参数3:拒签后消息是否重回队列
       */
      channel.basicNack(deliveryTag,true,true);
     }
   }
}

四、消费端限流

(说白了其实如果不设置限流,MQ其实只是一个管道他是不存消息的,即使发送者发了5000000条数据,MQ也是将这些数据全部分发给监听队列的监听者,只是通过交换机和队列去发送这些消息,并不存储。当设置了限流消费者每次最多只能拉去几条消息,那么才发挥了它的作用,就会把消息存储到队列里面,你消费完了再来取,不需要一次性全拿走造成消费者端服务器压力)

为了解决类似于秒杀这种瞬间过来大量请求造成我们服务端可能内存溢出这种情况,比如瞬间过来上百万的消息需要服务端处理,服务端拉去了上百万消息造成内存溢出服务崩了这种情况。此时就可以用到MQ的限流进行处理,其原理就是将消息不要全部拉去到我们的消费者服务端造成大量堆积,而是将消息堆积到MQ中,设置每次最多拉去多少条的消息慢慢处理。
(此时就出现一个面试题:消息都堆积到MQ那么MQ挂了怎么办消息丢了?答:见上章消息的可靠性传递)

在这里插入图片描述
实战写法:
1、想用限流必须开启手动签收,设置最多拉去几条消息 签收后不满几条才会再次去MQ中拉去

spring:
  rabbitmq:
   host: 192.168.0.162
   port: 5672
   username: itbaizhan
   password: itbaizhan
   virtual-host: /
   listener:
    simple:
    # 限流机制必须开启手动签收
     acknowledge-mode: manual
    # 消费端最多拉取5条消息消费,签收后不满5条才会继续拉取消息。
     prefetch: 5

2、消费者服务端只需要正常监听就好了正常去签收消息,只要配置文件配置好手动签收和一次最多拉去数量就OK

@Component
public class QosConsumer{
  @RabbitListener(queues = "my_queue")
  public void listenMessage(Message message, Channel channel) throws IOException, InterruptedException {
    // 1.获取消息
    System.out.println(new String(message.getBody()));
    // 2.模拟业务处理
    Thread.sleep(3000);
    // 3.签收消息
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
   }
}

五、实现不公平分发防止服务器空闲

背景:在RabbitMQ中,多个消费者监听同一条队列,则队列默认采用的轮询分发。但是在某种场景下这种策略并不是很好,例如消费者1处理任务的速度非常快,而其他消费者处理速度却很慢。此时如果采用公平分发,则消费者1有很大一部分时间处于空闲状态。此时可以采用不公平分发,即谁处理的快,谁处理的消息多。
可以说是利用限流机制实现不公平分发防止服务器空闲

配置不公平分发
核心:在于开启限流机制 每次消费者只拉一条谁消费完了谁去再拉消息,谁响应速度快谁就可以多拉多消费

spring:
  rabbitmq:
   host: 192.168.0.162
   port: 5672
   username: itbaizhan
   password: itbaizhan
   virtual-host: /
   listener:
    simple:
    # 限流机制必须开启手动签收
     acknowledge-mode: manual
    # 消费端最多拉取1条消息消费,这样谁处理的快谁拉取下一条消息,实现了不公平分发
     prefetch: 1

六、设置队列所有消息的存活时间

就是在创建队列的时候也就是在生产者的创建交换机和队列的配置类里面设置这个队列里每条消息的存活时间 .ttl(10000)

@Configuration
public class RabbitConfig2 {
  private final String EXCHANGE_NAME="my_topic_exchange2";
  private final String QUEUE_NAME="my_queue2";


  // 1.创建交换机
  @Bean("bootExchange2")
  public Exchange getExchange2(){
    return ExchangeBuilder
         .topicExchange(EXCHANGE_NAME)
         .durable(true).
        build();
   }


  // 2.创建队列
  @Bean("bootQueue2")
  public Queue getMessageQueue2(){
    return QueueBuilder
         .durable(QUEUE_NAME)
         .ttl(10000) //队列的每条消息存活10s
         .build();
   }


  // 3.将队列绑定到交换机
  @Bean
  public Binding bindMessageQueue2(@Qualifier("bootExchange2") Exchange exchange, @Qualifier("bootQueue2") Queue queue){
    return BindingBuilder.bind(queue).to(exchange).with("my_routing").noargs();
   }
}

七、优先队列

就是创建队列时加一个参数.maxPriority(10) 数值越大越优先消费
假设在电商系统中有一个订单催付的场景,即客户在一段时间内未付款会给用户推送一条短信提醒,但是系统中分为大型商家和小型商家。比如像苹果,小米这样大商家一年能给我们创造很大的利润,所以在订单量大时,他们的订单必须得到优先处理,此时就需要为不同的消息设置不同的优先级,此时我们要使用优先级队列。

就是在创建队列的时候对该队列进行设置个参数 .maxPriority(10) 数值越大越优先消费
当然了也可以在发送消息的时候对某一条特定的消息设置优先级 就跟设置消息的存活时间类似就是一个参数

八、死信队列

在MQ中,当消息成为死信(Dead message)后,消息中间件可以将其从当前队列发送到另一个队列中,这个队列就是死信队列。而在RabbitMQ中,由于有交换机的概念,实际是将死信发送给了死信交换机(Dead Letter Exchange,简称DLX)。死信交换机和死信队列和普通的没有区别。

在这里插入图片描述
消息成为死信的情况:

  1. 队列消息长度到达限制。
  2. 消费者拒签消息,并且不把消息重新放入原队列。
  3. 消息到达存活时间未被消费。
    实战写法:

因为死信队列和死信交换机跟普通的没有区别所以创建方式都相同
唯一不同的是在创建一个普通队列时这个普通队列要去绑定私信交换机 在创建这个队列时需要做些操作
他需要设置这个消息存活时间或者队列长度 来用于判断消息什么时候算是死的,最主要的是要设置死信交换机和死信 队列的路由关键字 这样当消息死了他才知道发给哪个交换机 并且把路由关键字也带着死信交换机才能知道发给哪个队列

// 普通队列
  @Bean(NORMAL_QUEUE)
  public Queue normalQueue(){
    return QueueBuilder
         .durable(NORMAL_QUEUE)
         .deadLetterExchange(DEAD_EXCHANGE) // 绑定死信交换机
         .deadLetterRoutingKey("dead_routing") // 死信队列路由关键字
         .ttl(10000) // 消息存活10s
         .maxLength(10) // 队列最大长度为10
         .build();
   }

九、延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
例如:用户下单后,30分钟后查询订单状态,未支付则会取消订单。
通过死信队列实现延迟队列的效果(RabbitMQ没有自带延迟交换机 需要安装插件)

在这里插入图片描述
但RabbitMQ中并未提供延迟队列功能,我们可以使用死信队列实现延迟队列的效果。
在这里插入图片描述
实战写法:

  1. 创建rabbitConfig配置类 配置普通队列和死信队列并且进行绑定

(普通队列设置消息存活时间为30分钟 到期后自动进入死信交换机中)

package com.example.rabbitmqorder.config;

import com.rabbitmq.client.AMQP;
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 RabbitmqConfig {
    //死信队列和交换机
    private final String DEAD_EXCHANGE="dead_exchange";
    private final String DEAD_QUEUE="dead_queue";
    //正常队列和交换机
    private final String NORMAL_EXCHANGE="normal_exchange";
    private final String NORMAL_QUEUE="normal_queue";
    //创建死信交换机
    @Bean(DEAD_EXCHANGE)
    public Exchange deadExchange(){
        return ExchangeBuilder.topicExchange(DEAD_EXCHANGE).durable(true).build();
    }
    //创建死信队列
    @Bean(DEAD_QUEUE)
    public Queue deadQueue(){
        return QueueBuilder.durable(DEAD_QUEUE).build();
    }
    //死信交换机绑定死信队列
    @Bean
    public Binding deadBinding(@Qualifier(DEAD_QUEUE) Queue queue,@Qualifier(DEAD_EXCHANGE) Exchange exchange){
       return BindingBuilder.bind(queue).to(exchange).with("dead_routing").noargs();
    }
    //创建普通交换机
    @Bean(NORMAL_EXCHANGE)
    public Exchange normalExchange(){
        return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE).durable(true).build();
    }
    //创建普通队列
    @Bean(NORMAL_QUEUE)
    public Queue normalQueue(){
        return QueueBuilder.durable(NORMAL_QUEUE).ttl(10000).deadLetterExchange(DEAD_EXCHANGE).deadLetterRoutingKey("dead_routing").build();
    }
    //普通队列绑定普通交换机
    @Bean
    public Binding normalBinding(@Qualifier(NORMAL_QUEUE)Queue queue,@Qualifier(NORMAL_EXCHANGE)Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("").noargs();
    }

}
  1. 模拟下单的方法,下单成功后自动向正常队列里发消息
package com.example.rabbitmqorder.controller;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Order {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RequestMapping("/toOrder/{orderId}")
    public String toOrder(@PathVariable("orderId")String orderId){
        rabbitTemplate.convertAndSend("normal_exchange","","下单成功单号为"+orderId);
        return "下单成功单号为"+orderId;
    }
}
  1. 监听业务操作
    监听死信队列 实现了延迟队列的效果,进行自己的业务处理过半个小时看订单状态之类的
package com.example.rabbitmqorder.controller;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class jiankucun {
    @RabbitListener(queues = "dead_queue")
    public void jianku(String message){
        System.out.println("接收到消息进行判断是否减库存还是取消订单 订单号为"+message);
    }
}

十、解决单点故障和吞吐量搭建集群

搭建了集群后,虽然多个节点可以互相通信,但队列只保存在了一个节点中,如果该节点故障,则整个集群都将丢失消息。一个MQ挂了他里面的队列消息也访问不了了并未解决问题
此时出现了镜像队列
此时我们需要引入镜像队列机制,它可以将队列消息复制到集群中的其他节点上。如果一个节点失效了,另一个节点上的镜像可以保证服务的可用性。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值