SpringCloud:初识MQ

84 阅读10分钟

初识MQ

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

1. 同步通讯

就像是打电话,实时调用

  • 同步调用的优点

    1. 失效性强,能立即得到结果
  • 同步调用存在的问题

    1. 耦合度高

      耦合度高从三个方面去考虑。

      • 第一点时间耦合:实体A给实体B发出指令后,需要等待实体B的响应。等待的时间取决于实体B对收到指令的处理时间。如果你需要对不同的实体按照顺序发出一系列的指令,等待的时间将是所有实体响应的时间。这就是时间耦合。像传统的瀑布模型。

      • 第二点空间耦合:实体A等待期间会占用一定的资源,例如运行中程序必定要维护自身的线程栈信息、堆内存资源、唤醒资源。等待本身没有问题,但是等待导致分配给实体A的资源没有被利用,在等待期间资源的利用率为零,同时需要额外的唤醒资源。实体A占用的资源空间,就好像另外一组出不了成果却一直拿着最高工资浑水摸鱼还要天天装模做样加班的同事,浪费。

      • 第三点协议耦合:实体A必须现实的调用实体B提供的接口,接口在哪里?接口的参数是什么?如果你需要对不同的实体按照顺序发出一系列的指令,不同的实体的接口和接口参数可能类似,从而出现耦合。(协议耦合找过资料但是依然不太了解,希望有大佬解答一下)

      我是参考这里(3条消息) 从"耦合"角度看异步交互模式_chengyan521489的博客-CSDN博客

    2. 性能下降

      从耦合段的时间耦合可以看出,调用的时间等于所有实体响应时间会十分影响性能的。如果多个实体响应一个实体的一些指令的时候可能还好,如果是多对多的情况可能变得更糟糕。更别说在真正开发中无论是实体的数量亦或者指令的数量都会大大增加,都使用同步调用那么性能会受到很大的影响。

    3. 资源浪费

      从耦合度的时间耦合和空间耦合看出,无论是等待时间去做更有意义的事情或用等待占用的资源空间去做跟有意义的事情,都要比光等着要好。如果可以让这部分的时间资源和空间资源自由都是非常好的选择。

    4. 级联失败

      同步调用对不同实体发出一系列指令,还要像传统的瀑布模型一样。如果在瀑布中的某一个环节出现了问题。那么出错环节往后的所有环节都不能进行下去。除非问题被解决,否则就死锁,嗯等。级联失败即使一步错,全停。

2. 异步通讯

就像是发短信

  • 异步调用常见实现就是事件驱动架构,实现图如下

    通过Broker广播实现异步调用。支付服务后通知BrokerBroker就会自动去调用其他微服务。支付服务不需要等待。如果Broker有多个请求但是其他服务没有空余的资源来处理,Broker还起到了缓冲的作用。

    image-20220618111027342

  • 异步调用的优点

    1. 服务解耦:如果增添服务,只需要在Broker添加订阅事件
    2. 性能提升,吞吐量提高:其他服务可以等Broker累计的服务到最大服务量的时候再去订阅服务,就是等到多活的的时候在做,提高资源的利用率,对于整体而言性能提升,吞吐量增加。
    3. 没有强依赖关系,不担心级联失败问题:异步,对于一个系统来说,异步操作可以很好的解耦合,因为每一步操作不需要等待结果即可继续往下进行,不论中间操作是否成功。
    4. 流量削峰:当支付请求数量过多的时候,其他服务可能没有那么多的资源来完成,那么多出来没办法完成的请求怎么办呢?Broker就起到了保管多余请求的缓冲作用。Broker就是起到了流量削峰的作用,让其他服务的资源不会接收到过载的服务量。
  • 异步调用的缺点

    1. 依赖于Broker的可靠性、安全性、吞吐性
    2. 架构复杂了,业务没有了明显的流程线,不好追踪管理

**注意:**同步调用更加常用,因为日常中时效性 > 并发性。但在面对高并发的情况下可以使用异步调用。(复杂是万恶之源)

3. MQ常见框架

MQ(MessageQueue),就是消息队列,字面来看就是用来存放消息的队列。也是事件驱动框架中的Broker

image-20220618113827580

RabbitMQ快速入门

1. RabbitMQ概念

image-20220618154310916

  • RabbitMQ中的几个概念:
    • channel:操作MQ的工具
    • exchange:路由消息到队列中
    • queue:缓存消息
    • virtual host:虚拟主机,是对queueexchange等资源的逻辑分组

2. 常见消息模型

  1. 基本消息队列BasicQueueimage-20220618155016678

  2. 工作消息队列WorkQueueimage-20220618155052794

  3. 发布订阅Publish/Subscribe,根据交换机不同分成三种

    1. 广播Fanout Exchangeimage-20220618155102253
    2. 路由Direct Exchangeimage-20220618155112393
    3. 主题Direct Exchangeimage-20220618155120877

3. 快速入门

手搓基本消息队列BasicQueue

从常见信息模型的基本消息队列的例图可以看出,整个基本消息队列框架是由一个发送者和一个消费者组成。下面是演示代码

  • 文件树image-20220618163635783

  • SpringCloud_MQ依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>
    
  • publisher使用默认引导类、配置,只有测试类需要修改

    package com.hyz;
    
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author workplace
     * @date 2022/6/18 16:05
     */
    @SpringBootTest
    public class PublisherTest {
        @Test
        public void testSendMessage() throws IOException, TimeoutException {
            // 1.建立连接
            ConnectionFactory factory = new ConnectionFactory();
            // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
            factory.setHost("192.168.153.130");
            factory.setPort(5672);
            factory.setVirtualHost("/");
            factory.setUsername("rabbitmq");
            factory.setPassword("rabbitmq");
            // 1.2.建立连接
            Connection connection = factory.newConnection();
    
            // 2.创建通道Channel
            Channel channel = connection.createChannel();
    
            // 3.创建队列
            String queueName = "simple.queue";
            channel.queueDeclare(queueName, false, false, false, null);
    
            // 4.发送消息
            String message = "hello, rabbitmq!";
            channel.basicPublish("", queueName, null, message.getBytes());
            System.out.println("发送消息成功:【" + message + "】");
    
            // 5.关闭通道和连接
            channel.close();
            connection.close();
    
        }
    }
    
  • consumer使用默认引导类、配置,只有测试类需要修改

    package com.hyz;
    
    import com.rabbitmq.client.*;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author workplace
     * @date 2022/6/18 16:10
     */
    @SpringBootTest
    public class ConsumerTest {
        public static void main(String[] args) throws IOException, TimeoutException {
            // 1.建立连接
            ConnectionFactory factory = new ConnectionFactory();
            // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
            factory.setHost("192.168.153.130");
            factory.setPort(5672);
            factory.setVirtualHost("/");
            factory.setUsername("rabbitmq");
            factory.setPassword("rabbitmq");
            // 1.2.建立连接
            Connection connection = factory.newConnection();
    
            // 2.创建通道Channel
            Channel channel = connection.createChannel();
    
            // 3.创建队列
            String queueName = "simple.queue";
            channel.queueDeclare(queueName, false, false, false, null);
    
            // 4.订阅消息
            channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope,
                                           AMQP.BasicProperties properties, byte[] body) throws IOException {
                    // 5.处理消息
                    String message = new String(body);
                    System.out.println("接收到消息:【" + message + "】");
                }
            });
            System.out.println("等待接收消息。。。。");
        }
    }
    

基本消息队列的消息发送流程:

  1. 建立 connection
  2. 创建channel
  3. 利用channel声明队列
  4. 利用channel向队列发送消息

基本消息队列的消息接收流程

  1. 建立 connection
  2. 创建channel
  3. 利用channel声明队列
  4. 定义 connection的消费行为handleDelivery()
  5. 利用channel将消费者和队列绑定

SpringAMQP

官方文档地址:Spring AMQP

1. Basic Queue 简单队列模型

image-20220618174713975

  • 操作步骤:

    1. 在父工程引入依赖。因为无论是发送抑或是消费都需要用到SpringAMQP,所以直接在父工程引入

      <!--AMQP依赖,包含RabbitMQ-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
      
    2. publisher配置文件中添加mq连接信息

      spring:
        rabbitmq:
          # 主机名
          host: 192.168.153.130
          # 端口
          port: 5672
          # 虚拟主机
          virtual-host: /
          # 用户名
          username: rabbitmq
          # 密码
          password: rabbitmq
      
    3. publisher服务中新建一个测试类,编写测试方法:

      package com.hyz.spring;
      
      import org.junit.jupiter.api.Test;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      
      /**
       * @author workplace
       * @date 2022/6/18 16:59
       */
      @SpringBootTest
      public class SpringAmpq {
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
          @Test
          public void test() {
              // 选择队列
              String queueName = "simple.queue";
              // 输入信息,未来可能是对象或某一个任务
              String message = "hyz1";
              rabbitTemplate.convertAndSend(queueName, message);
          }
      
      }
      

      注意: 如果发现消息队列为空,检查队列名称是否正确,customer是否结束运行。

    4. customer配置文件中添加mq连接信息

      spring:
        rabbitmq:
          # 主机名
          host: 192.168.153.130
          # 端口
          port: 5672
          # 虚拟主机
          virtual-host: /
          # 用户名
          username: rabbitmq
          # 密码
          password: rabbitmq
      
    5. consumer中编写消费逻辑,监听simple.queue消息队列。需要将监听类加入Spring容器当中,并且通过@RabbitListener(queues = "simple.queue")指定监听信息队列。方法的参数对应着信息的类型(如果以后传入对象就可以用兑现来接收)。

      package com.hyz.listener;
      
      import org.springframework.amqp.rabbit.annotation.RabbitListener;
      import org.springframework.stereotype.Component;
      
      /**
       * @author workplace
       * @date 2022/6/18 17:21
       */
      @Component
      public class SpringRabbitListener {
          /**
           * 通过@RabbitListener(queues = "simple.queue")指定监听的消息队列
           *
           * @param msg 类型对应着传入消息队列的内容类型
           */
          @RabbitListener(queues = "simple.queue")
          public void listenerMQ(String msg) {
              System.out.println(msg);
          }
      }
      
    6. 启动consumer的引导类,自动将消息队列的内容取出。

      消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能。

2. Work Queue 工作队列模型

image-20220618174735748

  • 操作步骤

    1. 在父工程引入依赖。因为无论是发送抑或是消费都需要用到SpringAMQP,所以直接在父工程引入

      <!--AMQP依赖,包含RabbitMQ-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
      
    2. publisher配置文件中添加mq连接信息

      spring:
        rabbitmq:
          # 主机名
          host: 192.168.153.130
          # 端口
          port: 5672
          # 虚拟主机
          virtual-host: /
          # 用户名
          username: rabbitmq
          # 密码
          password: rabbitmq
      
    3. publisher服务中新建一个测试类,编写测试方法:

      package com.hyz.spring;
      
      import org.junit.jupiter.api.Test;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      
      /**
       * @author workplace
       * @date 2022/6/18 16:59
       */
      @SpringBootTest
      public class SpringAmpq {
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
          @Test
          public void simpleQueueTest() throws InterruptedException {
              // 选择队列
              String queueName = "simple.queue";
              // 输入信息,未来可能是对象或某一个任务
              String message = "hyz__";
              int i = 0;
              while (i<=50) {
                  rabbitTemplate.convertAndSend(queueName, message + i);
                  i++;
                  Thread.sleep(20);
              }
          }
      }
      

      注意: 如果发现消息队列为空,检查队列名称是否正确,customer是否结束运行。

    4. customer配置文件中添加mq连接信息

      spring:
        rabbitmq:
          # 主机名
          host: 192.168.153.130
          # 端口
          port: 5672
          # 虚拟主机
          virtual-host: /
          # 用户名
          username: rabbitmq
          # 密码
          password: rabbitmq
      
    5. consumer中编写消费逻辑,监听simple.queue消息队列。需要将监听类加入Spring容器当中,并且通过@RabbitListener(queues = "simple.queue")指定监听信息队列。方法的参数对应着信息的类型(如果以后传入对象就可以用兑现来接收)。

      package com.hyz.listener;
      
      import org.springframework.amqp.rabbit.annotation.RabbitListener;
      import org.springframework.stereotype.Component;
      
      /**
       * @author workplace
       * @date 2022/6/18 17:21
       */
      @Component
      public class SpringRabbitListener {
          /**
           * 测试 WorkQueue
           *
           * @param msg 信息
           */
          @RabbitListener(queues = "simple.queue")
          public void listenerWorkQueue1(String msg) throws InterruptedException {
              System.out.println("消费者1处理:" + msg + "......" + LocalTime.now());
              Thread.sleep(20);
          }
      
          @RabbitListener(queues = "simple.queue")
          public void listenerWorkQueue2(String msg) throws InterruptedException {
              System.err.println("消费者2...处理:" + msg + "......" + LocalTime.now());
              Thread.sleep(100);
          }
      }
      
  • 结果:

    image-20220618182549241

    image-20220618182530705

    我们可以发现,最后的消耗的时间是大大超出我们的预期,并且最后都是在等待效率低的服务。

    这和异步通讯的优点是违背的。原因就是当多个消息送到消息队列中,不同的服务会依次均分获取消息,这种未开始工作先拿消息的动作称为预取信息

    这种情况下效率高的服务就会很快的将获取的任务完成,而效率低的服务则需要更长的时间。我们想要的是根据服务的效率分配对应的消息量。

    最后通过限制服务的预取消息数量少量多次分批获取

    这样子完成效率高的服务完成消息后能再向消息队列获取消息。

  • 通过修改customer的配置文件设置预取消息数量

    spring:
      rabbitmq:
        # 主机名
        host: 192.168.153.130
        # 端口
        port: 5672
        # 虚拟主机
        virtual-host: /
        # 用户名
        username: rabbitmq
        # 密码
        password: rabbitmq
        listener:
          simple:
            # 限制预取消息数量为 : 1
            prefetch: 1
    
  • 结果

    image-20220618182956580

    image-20220618183006246

    可以看出修改了预取消息数量之后的时间要快很多。