SpringCloud:SpringAMQP

130 阅读8分钟

SpringAMQP

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

官方文档地址: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

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

3. 发布、订阅模型-Fanout

  • 什么是发布订阅模式

    将消息传入交换机exchange,根据交换机的不同对信息进行不同的处理。

    image-20220621221313018

    **注意:**交换器会把消息发送到所有连接的消息队列。将消费者绑定在消息队列上,将消息队列绑定在交换器上。

  • 接下来就看看发布订阅中的Fanout

    在后续的开发中可能出现同一条日志消息,分别实现保存到数据库和打印到控制台的操作

    如果继续使用原来的简单队列或工作队列可能不太能满足上述的情况。

    所以我们在发布过程中添加一个交换器,通过交换器的类型将消息同时传到不同的消费者手中。这就是发布订阅。

    image-20220621222456846

  • 操作步骤

    1. consumer服务中,声明队列、交换机并且将两者绑定

      通过配置类将消息队列绑定在交换机上

      package com.hyz.config;
      
      import org.springframework.amqp.core.Binding;
      import org.springframework.amqp.core.BindingBuilder;
      import org.springframework.amqp.core.FanoutExchange;
      import org.springframework.amqp.core.Queue;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @author HGD
       * @date 2022/6/21 22:26
       */
      @Configuration
      public class FanoutConfig {
          /**
           * 1. 创建 FanoutExchange(交换机)加入 spring 容器当中
           *
           * @return new FanoutExchange("交换机名称")
           */
          @Bean
          public FanoutExchange fanoutExchange() {
              return new FanoutExchange("fanoutExchange");
          }
      
          /**
           * 2. 创建 fanout.queue1(队列)。
           * 方法名称就是队列的唯一id
           *
           * @return new Queue("队列名称")
           */
          @Bean
          public Queue fanoutQueue1() {
              return new Queue("fanout.queue1");
          }
      
          /**
           * 3. 将队列和交换机绑定
           *
           * @param fanoutQueue1   队列名称(队列 id)
           * @param fanoutExchange 交换机名称(交换机 id)
           * @return return BindingBuilder.bind(队列名称).to(交换机名称);
           */
          @Bean
          public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
              return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
          }
      
          @Bean
          public Queue fanoutQueue2() {
              return new Queue("fanout.queue2");
          }
      
          @Bean
          public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
              return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
          }
      
      }
      

      **注意:**这里所有的消息队列类的类名就是消息队列的唯一id。交换器的类名也是唯一id。都可以在网页端查到。

    2. consumer服务中编写队列的方法,分别监听两个队列

      这里就像简单队列和工作队列一样,将消费者绑定在消息队列上

      package com.hyz.listener;
      
      import org.springframework.amqp.rabbit.annotation.RabbitListener;
      import org.springframework.stereotype.Component;
      
      import java.time.LocalTime;
      import java.util.Locale;
      
      /**
       * @author workplace
       * @date 2022/6/18 17:21
       */
      @Component
      public class SpringRabbitListener {
          @RabbitListener(queues = "fanout.queue1")
          public void listenerFanoutQueue1(String msg) {
              System.out.println(msg);
          }
      
          @RabbitListener(queues = "fanout.queue2")
          public void listenerFanoutQueue2(String msg) {
              System.err.println(msg);
          }
      }
      
    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 SpringAmpqTest {
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
          @Test
          public void sendFanoutExchangeTest() {
              // 1. 交换机名称
              String exchangeName = "fanoutExchange";
              // 2. 消息
              String message = "发送消息";
              rabbitTemplate.convertAndSend(exchangeName, "", message);
          }
      }
      
    4. 先启动customer,然后再启动publisher

4. 发布、订阅模型-Direct

  • 什么是路由交换器Direct Exchange

    消息队列设置一个BindingKey。当消息携带着key (routingKey)传入交换机,交换机会将消息传入BindingKeykey (routingKey)匹配的消息队列中。

    就是消息队列设置一个BindingKey,消息携带着key (routingKey)传入交换机,交换机将消息根据key传入对应的消息队列

    image-20220622205751251

    **注意:**不同消息队列的BindingKey是可以相同

  • 操作步骤:

    1. customer中利用@RabbitListener声明ExchangeQueueRoutingKey

      package com.hyz.listener;
      
      import org.springframework.amqp.core.ExchangeTypes;
      import org.springframework.amqp.rabbit.annotation.Exchange;
      import org.springframework.amqp.rabbit.annotation.Queue;
      import org.springframework.amqp.rabbit.annotation.QueueBinding;
      import org.springframework.amqp.rabbit.annotation.RabbitListener;
      import org.springframework.stereotype.Component;
      
      import java.time.LocalTime;
      import java.util.Locale;
      
      /**
       * @author workplace
       * @date 2022/6/18 17:21
       */
      @Component
      public class SpringRabbitListener {
          /**
           * 通过 @RabbitListener 设置消息队列、交换机、bindingKey。
           * value: 消息队列
           * exchange: 交换机,可以选择类型
           * key: bindingKey
           *
           * @param msg 从消息队中接受的信息
           */
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "direct.queue1"),
                  exchange = @Exchange(name = "directExchange", type = ExchangeTypes.DIRECT),
                  key = {"red", "blue"})
          )
          public void listenerDirectQueue1(String msg) {
              System.out.println("消费者1接受Direct消息---->" + msg);
          }
      
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "direct.queue2"),
                  exchange = @Exchange(name = "directExchange", type = ExchangeTypes.DIRECT),
                  key = {"yellow", "blue"})
          )
          public void listenerDirectQueue2(String msg) {
              System.err.println("消费者2接受Direct消息---->" + msg);
          }
      }
      
    2. publisher中编写测试方法,向directExchange发送消息

      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 SpringAmpqTest {
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
         /**
           * 通过 convertAndSend 的 routingKey 和消息队列的 bindingKey 匹配
           */
          @Test
          public void sendDirectExchangeTest() {
              // 1. 交换机名称
              String exchangeName = "directExchange";
              // 2. 消息
              String message = "blue";
              rabbitTemplate.convertAndSend(exchangeName, "blue", message);
          }
      }
      
    3. 先启动customer然后启动测试,看控制台是否有对应的结果

      image-20220622222406930

5. 发布、订阅模型-Topic

  • 什么是主题路由器Topic Exchange

    通过在消息队列的bindingKey使用通配符,匹配消息的key。通配符的*代表一个字符,#代表0~n个字符。

    image-20220622231040302

  • 操作步骤

    1. customer中利用@RabbitListener声明ExchangeQueueRoutingKey

      package com.hyz.listener;
      
      import org.springframework.amqp.core.ExchangeTypes;
      import org.springframework.amqp.rabbit.annotation.Exchange;
      import org.springframework.amqp.rabbit.annotation.Queue;
      import org.springframework.amqp.rabbit.annotation.QueueBinding;
      import org.springframework.amqp.rabbit.annotation.RabbitListener;
      import org.springframework.stereotype.Component;
      
      import java.time.LocalTime;
      import java.util.Locale;
      
      /**
       * @author workplace
       * @date 2022/6/18 17:21
       */
      @Component
      public class SpringRabbitListener {
          /**
           * 通过  @RabbitListener 设置消息队列、交换机、bindingKey
           * 和 direct 的差别在于 key 的不同。
           * 在 topic 的 key 是可以使用通配符。
           * '#' 代表 0 或多歌词
           * '*' 代表 一个词
           *
           * @param msg 从消息队中接受的信息
           */
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "topic.queue1"),
                  exchange = @Exchange(name = "topicExchange", type = ExchangeTypes.TOPIC),
                  key = "*.china.#"
          ))
          public void listenerTopicQueue1(String msg) {
              System.out.println("消费者1接受Topic消息---->" + msg);
          }
      
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "topic.queue2"),
                  exchange = @Exchange(name = "topicExchange", type = ExchangeTypes.TOPIC),
                  key = "#.news.*"
          ))
          public void listenerTopicQueue2(String msg) {
              System.err.println("消费者2接受Topic消息---->" + msg);
          }
      }
      
    2. publisher中编写测试方法,向topicExchange发送消息

      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 SpringAmpqTest {
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
          /**
           * 通过 convertAndSend 的 routingKey 和消息队列的 bindingKey 匹配
           */
          @Test
          public void sendTopicExchangeTest() {
              // 1. 交换机名称
              String exchangeName = "topicExchange";
              // 2. 消息
              String message = "中国新闻";
              rabbitTemplate.convertAndSend(exchangeName, "news.china", message);
          }
      }