RabbitMQ

同步异步

  • 微服务间通讯有同步和异步两种方式:
    • 同步通讯:就像打电话,需要实时响应
    • 异步通讯:就像发邮件,不需要马上回复。

同步通讯(同步通信)

  • 在之前的学习中,Feign调用就属于同步方式,虽然调用可以实时得到结果,但存在下面的问题:

  • 同步调用的优点

    • 时效性较强,可以立即得到结果
  • 同步调用的问题

    • 耦合度高
      • 每次加入新的需求,都要修改原来的代码
    • 性能和吞吐能力下降
      • 调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。
    • 有额外的资源消耗
      • 调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
    • 会产生级联失败的问题
      • 如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障

异步通讯(异步通信)

  • 异步通讯可完全避免同步通讯的问题,常见的异步调用实现方式为:事件驱动模式

    以购买商品为例进行解释说明:

    用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。

    在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。

    订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。如下图所示

  • 为了解除事件发布者与订阅者之间的耦合,两者并不进行直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。如下图所示

    Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。

  • 异步调用的优点

    • 耦合度极低,每个服务都可以灵活插拔,可替换
    • 性能和吞吐能力提高:无需等待订阅者处理完成,响应更快速
    • 故障隔离:服务之间没有直接调用(即不存在强依赖),不会产生级联失败的问题
    • 不存在额外的资源消耗:调用间没有阻塞,不会造成无效的资源占用
    • 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
  • 异步调用的缺点

    • 架构复杂了,业务没有明显的流程线,不好管理
    • 需要依赖于Broker的可靠、安全、性能
  • 注意:

    • 开源软件或云平台上 Broker 的软件是非常成熟的,比较常见的一种就是我们要学习的MQ技术。

MQ(消息队列)常见技术介绍

  • MQ(MessageQueue)即消息队列(就是事件驱动架构中的Broker),是分布式应用之间实现异步通信的一种方式,主要由三部分组成:

    • 生产者(Publisher):生产消息的一端,相当于消息的发送方,主要负责创建承载业务信息的消息

    • 消费者(Consumer):是消费消息的一端,主要是根据消息中所包含的业务信息来处理各种业务逻辑

    • 消费服务端(即消费代理)是消费队列(Queue)最核心的部分,主要负责接收、存储、路由和投递信息,确保生产者和消费者之间的可靠异步通信:

  • MQ的使用场景主要有三个:

    • 流量削峰:主要用于解决流量过大时业务需求要短时间响应的问题。也就是说当流量过大时,服务器性能无法满足,就会导致大量请求被积压,从而出现客户端等待超时的场景。为了保证高可用性,可以把请求先发送给消息队列,然后在由消息队列将其分发给其他服务器,实现对流量的一个缓冲。

    • 应用解耦:就是通过架构设计和技术手段来降低系统各模块服务之间的直接依赖,增强系统的容错性,可维护性以及可拓展性。你就比如说在单体架构中存在订单系统、物流系统和库存系统,它们按顺序执行,此时假设某一个系统代码出错就会导致整个系统崩溃。但是你将这三个系统解耦之后,即使某一个系统出问题,也不会影响其他系统,增强了系统的容错性。

    • 异步处理:主要应用于对时效性要求不高的场景。比如说订单系统、物流系统和库存系统,用户下完订单之后会直接返回下单成功通知,而订单系统会将消息发送到消息队列,后续物流系统和库存系统会从消息队列中拿出消息进行处理,这个过程用户是无感知的,可以增加用户

  • 几种常见MQ的对比:

    RabbitMQ ActiveMQ RocketMQ Kafka
    公司/社区 Rabbit Apache 阿里 Apache
    开发语言 Erlang Java Java Scala&Java
    协议支持 AMQP,XMPP,SMTP,STOMP OpenWire,STOMP,REST,XMPP,AMQP 自定义协议 自定义协议
    可用性 一般
    单机吞吐量 一般 非常高
    消息延迟 微秒级 毫秒级 毫秒级 毫秒以内
    消息可靠性 一般 一般
  • 追求高可用性:Kafka、 RocketMQ 、RabbitMQ

  • 追求高可靠性:RabbitMQ、RocketMQ

  • 追求高吞吐量(吞吐能力):RocketMQ、Kafka

  • 追求消息低延迟:RabbitMQ、Kafka

RabbitMQ简介

  • RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:https://www.rabbitmq.com/

  • RabbitMQ的原理

    • RabbitMQ是基于AMQP协议的分布式消息中间件。AMQP的主要工作机制是生产者首先会把消息发送给交换机,交换机接收到消息后会根据路由规则将其路由给绑定的消息队列,消息队列收到消息后又会把消息投递给订阅(监听)该消息的消费者
  • AMQP

  • RabbitMQ对应的架构如图:

    • 生产者(Publisher):生产消息的一端,相当于消息的发送方,主要负责创建承载业务信息的消息

    • 消费者(Consumer):是消费消息的一端,主要是根据消息中所包含的业务信息来处理各种业务逻辑

    • 队列(queue):存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理

    • 交换机(exchange):负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。

    • 虚拟主机(virtual host):起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue,是对queue、exchange等资源的逻辑分组

RabbitMQ安装

单机部署

在Centos7虚拟机中使用Docker来安装

注意:docker相关技术点可详见Docker部分内容

  • Step1: 查看Docker库是否有rabbitmq镜像并拉取镜像

    shell 复制代码
    docker search rabbitmq
    docker pull rabbitmq:4-management

    注意:

    ​ 若无法使用docker pull从官网拉取则可以将镜像包mq.tar传到linux中,然后利用docker load -i mq.tar命令来加载镜像到本地

  • Step2: 创建rabbitmq目录并进入到该目录下

    shell 复制代码
    mkdir ~/rabbitmq
    cd ~/rabbitmq

    注意:此处创建并进入相关目录下是为了后续配置数据卷,此处只进行了相关演示,并未进行数据卷配置,实际配置可详见相关技术博客

  • Step3: 创建并运行容器,同时设置端口映射、目录映射等

    注意:

    • 若想要加入的网络不存在可自行创建,具体操作可详见Docker完整技术汇总部分内容

    • 15672:RabbitMQ提供的管理控制台端口

    • 5672:RabbitMQ的消息发送处理接口(即消息通信接口)

    shell 复制代码
    docker run -d \
     --privileged=true \
     --name mq \
     --hostname mq1 \
     -p 15672:15672 \
     -p 5672:5672 \
     --restart=always \
     -e DEFAULT_USER=root \
     -e DEFAULT_PASS=123456 \
     --network webb \
     rabbitmq:4-management

    注意:

    • 必须添加--privileged=true,它的作用是让该容器获取宿主机的几乎所有权限;若不添加则无法访问RabbitMQ的管理控制台
    • --hostname用来配置主机名,后续主要用其进行集群部署,单机部署时不配置主机名也无所谓
  • Step4: RabbitMQ启动成功后,在浏览器输入所使用虚拟机的ip地址:端口号即可访问RabbitMQ控制台,如下图所示。

RabbitMQ管理控制台说明

输入用户名和密码之后即可进入到管理控制台页面,如下所示

  • 总览页面

  • 连接页面

  • 通道页面

  • 交换机页面

  • 队列页面

  • 管理页面(用来管理用户信息)

    • 可在管理页面中来创建新用户,步骤如图所示

    • 创建成功后,回到交换机页面发现,每个虚拟机都有自己的交换机类型,互不冲突,如下图所示

    • 注意:每个用户均有自己独享的虚拟机,而在创建用户时也添加了一个虚拟主机mq2,这个mq2除了被zhangsan使用外,还被名为root的用户使用,所以为了保证mq2被zhangsan独享,则操作步骤如下图所示:

集群部署

--暂略,后续补充--

RabbitMQ常见消息模型

  • RabbitMQ官方提供了6个不同的Demo示例,对应了不同的消息模型,分别为:基本消息队列、工作消息队列、发布订阅模式的消息队列(包含三种)、RPC模式消息队列

Basic Queue基本消息队列模型

  • 基本消息队列模型只包含三个角色:生产者(Publisher)、消费者(Consumer)以及消息队列(Queue)

相关API

  • ConnectionFactory

    构造函数 解释
    public ConnectionFactory() 创建连接工厂对象
    ConnectionFactory类的方法 解释
    public void setHost(String host) 配置RabbitMQ的所在的IP地址信息
    public void setPort(int port) 配置RabbitMQ的消息发送处理接口(即消息通信接口)
    public void setVirtualHost(String virtualHost) 配置用户所能使用的虚拟主机
    public void setUsername(String username) 配置RabbitMQ的用户名
    public void setPassword(String password) 配置RabbitMQ的密码
    public Connection newConnection() throws IOException, TimeoutException 通过连接工厂对象创建Connection连接对象
  • Connection接口

    方法 解释
    Channel createChannel() throws IOException 创建Channel通道对象
    void close() throws IOException
  • Channel接口

    方法 解释
    AMQP.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException 检查某个队列是否已存在。若存在,则不会重复创建;若不存在则创建指定队列名称的队列
    void basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body) throws IOException 通过指定的路由键将消息发送到某个交换机
    String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException 启动消费者监听指定队列中的消息
    • AMQP.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)

      参数 解释
      queue 队列名称;如果为空字符串 "",则由服务器自动生成一个唯一的队列名称(常用于临时队列)
      durable 队列是否为持久化队列。若是,则队列在服务器重启后仍存在(需搭配消息持久化使用才有效);若不是,则队列在服务器重启后会被销毁
      exclusive 指定队列是否为排他队列。即 是否只限当前连接访问该队列,并在连接关闭时自动删除该队列
      autoDelete 当最后一个消费者取消/断开连接时,是否自动删除队列
      arguments(Map<String, Object>) 配置队列的其它属性参数,比如: x-message-ttl: 消息存活时间(毫秒) x-expires: 队列未使用时的过期时间(毫秒) x-max-length: 队列最大消息数 x-max-length-bytes: 队列最大容量(字节) x-dead-letter-exchange: 死信交换机 x-dead-letter-routing-key: 死信路由键
    • void basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body)

      参数 解释
      exchange 指定消息要发送到的交换机名称。如果是空字符串 "",表示使用默认交换机(即Direct Exchange路由交换机)
      routingKey 路由键
      props 消息的元数据属性,例如持久性、编码、内容类型、消息ID、时间戳、自定义headers等
      body 消息体(内容)以字节数组的形式传入,通常使用字符串的 getBytes("UTF-8") 方法编码得到
    • String basicConsume(String queue, boolean autoAck, Consumer callback)---该方法有多个重载版本

      参数 解释
      queue 指定要监听的队列名称。该队列必须已经存在,或者你提前用 queueDeclare 方法创建该队列。
      autoAck 是否开启ACK自动确认消息机制。若为false,则为手动确认,此时接收到消息后,需显示调用Channel接口下的basicAck方法来确认消息
      callback 一个实现了 Consumer 接口的对象,用来处理收到的消息。需要重写该接口中的handleDelivery()方法来处理接收到的消息
    • public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException

      参数 解释
      consumerTag 消费者唯一标识,可通过 basicConsume() 时传入或自动生成
      envelope 包含交付信息,比如:消息唯一标识、来源的交换机等等
      properties 包含消息元信息(contentType、headers、持久性等)
      body 消息内容(注意:需自己进行转码处理)

示例

该示例Demo可详见Gitee分支BasicQueue
示例:生产者向消息队列发送"HelloWorld!!!",然后由消费者从消息队列接收到消息并在控制台输出

  • 公共步骤:在父工程中引入依赖:spring-amqp

    注意:必须导入该依赖,否则生产者和消费者均无法获取到ConnectionFactory对象并建立连接

    xml 复制代码
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  • 生产者(Publisher)代码步骤及示例

    • Step1: 利用ConnectionFactory建立Connection连接

    • Step2: 创建通道Channel

    • Step3: 利用Channel声明队列(即订阅队列或绑定队列)

    • Step4: 利用Channel发送消息到队列

    • Step5: 关闭连接和Channel

    java 复制代码
    package at.guigu.mq;
    
    import org.junit.jupiter.api.Test;
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    /**
     * 生产者消息发送
     */
    public class PublisherTest {
        @Test
        public void testSendMessage() throws IOException, TimeoutException {
            // 1.通过连接工厂建立连接
            ConnectionFactory factory = new ConnectionFactory(); // 创建连接工厂对象
            // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
            factory.setHost("192.168.10.100"); // 配置RabbitMQ的所在的IP地址信息
            factory.setPort(5672); // 配置RabbitMQ的消息发送处理接口(即消息通信接口)
            factory.setVirtualHost("/"); // 配置用户所能使用的虚拟主机
            factory.setUsername("root"); // 配置RabbitMQ的用户名
            factory.setPassword("123456"); // 配置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)代码步骤及示例

    • 利用ConnectionFactory建立Connection连接
    • 创建通道Channel
    • 利用Channel声明队列(即订阅队列或绑定队列)
    • 订阅消息并重写Channel接口中的handleDelivery方法来接收并处理消息
    java 复制代码
    package at.guigu.mq;
    
    import com.rabbitmq.client.*;
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    public class ConsumerTest {
    
        public static void main(String[] args) throws IOException, TimeoutException {
            // 1.通过连接工厂建立连接
            ConnectionFactory factory = new ConnectionFactory(); // 创建连接工厂对象
            // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
            factory.setHost("192.168.10.100"); // 配置RabbitMQ的所在的IP地址信息
            factory.setPort(5672); // 配置RabbitMQ的消息发送处理接口(即消息通信接口)
            factory.setVirtualHost("/"); // 配置用户所能使用的虚拟主机
            factory.setUsername("root"); // 配置RabbitMQ的用户名
            factory.setPassword("123456"); // 配置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.订阅消息队列并通过匿名内部类对象DefaultConsumer来获取到消息进行处理
                // handleDelivery方法:用于处理生产者发送到已订阅的消息队列中的消息---只要检测到订阅的消息队列中存在消息就会进行消费
            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("等待接收消息。。。。");
        }
    }
  • 注意:

    • 在生产者和消费者的代码中都存在创建队列的代码,原因:

      • 生产者和消费者的启动顺序是不确定的,所以两边都需要存在创建队列的代码,来防止消息队列不存在。若某一方已创建消息队列,则另一方不会再次创建。
    • 在消费者的代码中首先会执行System.out.println("等待接收消息。。。。");,然后再去执行System.out.println("接收到消息:【" + message + "】");,原因:

      • Channel接口下的basicConsume方法作用是启动消费者监听指定队列中的消息。在消费者第一次使用basicConsume方法时,此时消息并未被投递到消费者,只启动了消费者对执行消息队列的监听,由于此时被监听的消息队列还并未将消息投递给消费者,所以此时并不会执行handleDelivery()方法中的System.out.println("接收到消息:【" + message + "】"),而是会执行System.out.println("等待接收消息。。。。"),在执行完毕后,此时消息队列中的消息已经被投递给消费者了,最后才会去对消息进行处理。

SpringAMQP

  • SpringAMQP是基于RabbitMQ封装的一套模板,并且还利用SpringBoot对其实现了自动装配,使用起来非常方便。官网地址:https://spring.io/projects/spring-amqp

  • SpringAMQP提供了三个功能:

    • 基于注解的监听器模式,可异步接收消息并处理
    • 封装了RabbitAdmin工具类,用于自动声明队列、交换机及绑定关系
    • 封装了RabbitTemplate工具类,用于发送和接收消息

Basic Queue基本队列模型示例

该示例Demo可详见Gitee分支BasicQueue
案例:利用SpringAMQP实现HelloWorld中的基础消息队列功能

示例:生产者向消息队列发送"HelloWorld!!!",然后由消费者从消息队列接收到消息并在控制台输出

步骤:

​ 1.父工程中引入依赖

​ 2.在publisher服务中利用RabbitTemplate工具类发送消息到simple.queue这个消息队列

​ 3.在consumer服务中编写消费逻辑,并绑定到simple.queue这个消息队列进行监听

  • 公共步骤

    • Step1: 在父工程中引入依赖:spring-amqp

      xml 复制代码
      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
    • Step2: 在生产者模块(即publisher模块)和消费者模块(即consumer模块)的配置文件application.yml中添加mq连接信息

      yml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.10.100 # rabbitMQ的ip地址
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
  • 生产者(Publisher)代码步骤及示例

    • Step1: 在生产者模块(即publisher模块)中创建测试类SpringAmqpPubTest,测试代码如下:

      java 复制代码
      package at.guigu.mq;
      
      import org.junit.jupiter.api.Test;
      import org.junit.runner.RunWith;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      
      /**
       * 生产者消息发送
       */
      @RunWith(SpringRunner.class)
      @SpringBootTest
      class SpringAmqpPubTest {
          @Autowired
          private RabbitTemplate rabbitTemplate; // RabbitTemplate用于发送和接收消息
          @Test
          void testSimpleQueue() {
              String queueName = "simple.queue"; // 消息队列名
              String message = "HelloWorld!!!"; // 消息
              rabbitTemplate.convertAndSend(queueName, message); // 将消息发送到指定消息队列
          }
      }

      注意:发送消息到指定消息队列时需保证队列存在,若队列不存在则需先创建队列

    • Step2: 运行测试类成功后,在图一可看出消息已成功发送到消息队列中,单击图一中的消息队列进入指定消息队列simple.queue中,单击Get Message(s)即可看到已接收到的消息(如图二所示)

  • 消费者(Consumer)代码步骤及示例

    • Step1: 在消费者模块(即consumer模块)中新建一个监听类SpringRabbitAMQPListener,编写消费逻辑

      java 复制代码
      package at.guigu.mq.listener;
      
      import org.springframework.amqp.rabbit.annotation.RabbitListener;
      import org.springframework.stereotype.Component;
      
      @Component
      public class SpringRabbitAMQPListener {
          // 指定监听的消息队列---被监听的队列的消息会自动传入被@RabbitListener注解修饰的方法中作为该方法的参数
          @RabbitListener(queues = "simple.queue")
          public void listenSimpleQueueMessage(String msg) throws InterruptedException {
              System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
          }
      }

      注意:

      ​ 1.生产者发送的消息类型与监听类中接收消息的参数的类型要保持一致。

      ​ 2.被@RabbitListener注解所修饰的类的参数就是消息,参数类型与消息类型一致

    • Step2: 运行消费者模块(即consumer模块)的启动类,运行成功后可从图一看到成功接收到消息并处理。此时在RabbitMQ管理控制台可看到(图二),被消费成功的消息会被清除。

    • 注意: 消息一旦被消费,就会从队列中被删除,RabbitMQ没有消息回溯功能

可能出现的问题

  • 运行测试类时若报如下错误:

    原因:未配置数据源相关的url

    注意:mq是不需要配置数据源的,但是 Spring Boot 自动配置机制 默认需要一个数据源配置,所以解决方式如下:

    • 方法:在对应服务的启动类中,给@SpringBootApplication注解添加exclude属性且属性值为DataSourceAutoConfiguration.class,以此来排除数据源自动配置

      java 复制代码
      package at.guigu.mq;
      
      import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
      import org.springframework.amqp.support.converter.MessageConverter;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
      import org.springframework.context.annotation.Bean;
      
      @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
      public class PublisherApplication {
          public static void main(String[] args) {
              SpringApplication.run(PublisherApplication.class);
          }
      }

Work Queue工作队列模型

当消息处理比较耗时的时候,可能导致生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。因此就有了Work Queue工作消息队列模型,此时多个消费者就能共同处理消息,速度就能大大提高了。

  • Work queues,也被称为(Task queues),任务模型。简单来说就是 让多个消费者绑定到一个队列,共同消费队列中的消息, 同一个消息只会被一个消费者处理

    • 作用:提高消息处理的速度,避免队列的消息堆积

示例

该示例Demo可详见Gitee分支WorkQueue
​ 1.在生产者模块(即publisher模块)中定义测试方法,每秒产生50条消息发送到消息队列simple.queue中

​ 2.在消费者模块(即consumer模块)中定义两个均监听消息队列simple.queue的消息监听者

​ 3.消费者1每秒处理50条消息,消费者2每秒处理10条消息

  • 公共步骤

    • Step1: 在父工程中引入依赖:spring-amqp

      xml 复制代码
      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
    • Step2: 在生产者模块(即publisher模块)和消费者模块(即consumer模块)的配置文件application.yml中添加mq连接信息

      yml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.10.100 # rabbitMQ的ip地址
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
  • 生产者(Publisher)代码步骤及示例

    • Step1: 在生产者模块(即publisher模块)中创建测试类SpringAmqpWqPubTest,测试代码如下:

      java 复制代码
      package at.guigu.mq;
      
      import org.junit.jupiter.api.Test;
      import org.junit.runner.RunWith;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      
      /**
       * Work Queue工作队列模型示例
       * 生产者消息发送:向队列中不停发送消息,模拟消息堆积
       */
      @RunWith(SpringRunner.class)
      @SpringBootTest
      class SpringAmqpWqPubTest {
          @Autowired
          private RabbitTemplate rabbitTemplate; // RabbitTemplate用于发送和接收消息
          @Test
          void testWorkQueue() throws InterruptedException {
              String queueName = "simple.queue"; // 消息队列名
              String message = "Hello, message_"; // 消息
              for (int i = 0; i < 50; i++) {
                  rabbitTemplate.convertAndSend(queueName, message + i); // 将消息发送到指定消息队列
                  Thread.sleep(20); // 暂时放弃对CPU的使用权,但不会释放锁。作用:让当前线程由可运行态转换为有时限的等待,超时时间到自动恢复为可运行状态
              }
          }
      }
    • Step2: 运行测试类成功后,由图可看出消息已成功发送到消息队列中

  • 消费者(Consumer)代码步骤及示例

    • Step1: 在消费者模块(即consumer模块)的监听器listener包下新建一个监听类SpringRabbitWQListener,编写消费逻辑

      java 复制代码
      package at.guigu.mq.listener;
      
      import org.springframework.amqp.rabbit.annotation.RabbitListener;
      import org.springframework.stereotype.Component;
      import java.time.LocalTime;
      
      /**
       * Work Queue工作队列模型消费者示例
       * 消费者消息监听处理---消费者1每秒处理50条消息,消费者2每秒处理10条消息
       */
      @Component
      public class SpringRabbitWQListener {
          // 指定监听的消息队列---被监听的队列的消息会自动传入被@RabbitListener注解修饰的方法中作为该方法的参数
          @RabbitListener(queues = "simple.queue")
          public void listenWorkQueueMessage01(String msg) throws InterruptedException {
              System.out.println("消费者1...接收到simple.queue的消息:【" + msg + "】" + LocalTime.now());
              // 消费者1每秒处理50条消息
              Thread.sleep(20); // 暂时放弃对CPU的使用权,但不会释放锁。作用:让当前线程由可运行态转换为有时限的等待,超时时间到自动恢复为可运行状态
          }
      
          @RabbitListener(queues = "simple.queue")
          public void listenWorkQueueMessage02(String msg) throws InterruptedException {
              // 消费者2控制台红色打印
              System.err.println("消费者2...接收到simple.queue的消息:【" + msg + "】" + LocalTime.now());
              // 消费者2每秒处理10条消息
              Thread.sleep(100); // 暂时放弃对CPU的使用权,但不会释放锁。作用:让当前线程由可运行态转换为有时限的等待,超时时间到自动恢复为可运行状态
          }
      }
    • Step2: 运行消费者模块(即consumer模块)的启动类,运行成功后可从图中看到成功接收到消息并处理。

      • 但是有个问题: 虽然消息被成功处理,但时间超过了1s,原因是:
        • 消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。也就是说,消费者1和消费者2各处理了25条消息
        • 它其实就是由于RabbitMQ内部的 轮询机制 造成的:生产者发送消息到消息队列时速度是很快的,队列接收到消息后会将消息以轮询的方式发送给监听该队列的消费者,因此不论消费者的消费能力有多强,均会被平等的雨露均沾,不论上一个消息是否已处理完毕
    • Step3: 为了解决Step3中的问题,则在消费者模块(即consumer模块)的配置文件application.yml中添加配置来开启 消息预取机制

      此时就是轮询+消息预取两个机制一起生效

      yaml 复制代码
      spring:
        rabbitmq:
          listener:
            simple:
              prefetch: 1 # 每次只能获取一条消息,处理完成并触发了ACK确认机制后,才能获取下一个消息

      消费者模块(即consumer模块)配置文件application.yml的最终完整代码如下

      yaml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.10.100 # rabbitMQ的ip地址
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
          listener:
            simple:
              prefetch: 1 # 每次只能获取一条消息,处理完成并触发了ACK确认机制后,才能获取下一个消息
    • Step4: 重新运行消费者模块(即consumer模块)的启动类,运行成功后可从图中看到此时在1s内即可完成所有消息的处理,并且消费者1处理消息的数量多于消费者2(即能者多劳)

发布-订阅模型

  • 发布-订阅模型与Basic Queue基本队列模型和Work Queue工作队列模型的区别在于它允许将同一个消息发送给多个消费者。主要原因是它多了一个交换机(Exchange),模型如图所示。

    注意:此时生产者(即消息发送者)不需要知道队列的存在

  • 发布订阅模型根据交换机的不同分为三类:

    • Direct Exchange(路由交换机):是完整匹配方式,即RoutingKeyBindingKey完全一致,相当于点对点发送。它会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式。

      • 每一个Queue都与Exchange设置一个BindingKey
      • 发送者发送消息时,需指定消息的RoutingKey
      • Exchange会将消息路由到BindingKey与消息RoutingKey一致的队列中
      • 需要注意的是,消息队列的BindingKey可以有多个。如果BindingKey一致,则Direct Exchange(路由交换机)可看作Fanout Exchange(广播交换机)
    • Fanout Exchange(广播交换机):具有广播机制,它会将消息发送给绑定到当前交换机上的所有消息队列中

    • Topic Exchange(主题交换机):是正则表达式匹配,它会根据RoutingKey设置一个正则表达式来匹配BindingKey,符合匹配规则的消息队列都会接收到该消息

  • 交换机是如何实现消息路由的方式

    • 交换机中订阅了消息的路由规则,消息的路由规则主要由交换机类型以及binding决定,binding指的是绑定,主要用于将交换机与指定的消息队列进行绑定,每个绑定关系都存在一个bindingkey,它就相当于在交换机中建立了一个路由表

    • 生产者发送消息时会携带一个routingkey,到达交换机后,交换机会拿到routingkey并与路由表中的bingingkey进行匹配,然后将其发送给指定的消息队列即可

      注意:匹配方式由交换机的类型决定,由此在发布订阅模式中实现了同一消息可以发送给多个消费者

Direct Exchange(路由交换机)示例

该示例Demo可详见Gitee分支FaBuDingYue/DirectExchange的提交记录
示例:

​ 1.在消费者模块(即consumer模块)中利用代码声明交换机、队列,并将两者绑定

​ 2.在消费者模块(即consumer模块)中编写两个消费者方法,分别监听消息队列fanout.queue1和fanout.queue2

​ 3.在生产者模块(即publisher模块)中定义测试方法,向交换机cgrs572.fanout中发送消息

  • 公共步骤

    • Step1: 在父工程中引入依赖:spring-amqp

      xml 复制代码
      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
    • Step2: 在生产者模块(即publisher模块)和消费者模块(即consumer模块)的配置文件application.yml中添加mq连接信息

      yml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.10.100 # rabbitMQ的ip地址
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
  • 消费者(Consumer)代码步骤及示例---方式一

    • Step1: 在消费者模块(即consumer模块)的监听器listener包下创建一个监听器类SpringRabbitListener,并在该类中添加两个方法listenDirectQueue1listenDirectQueue2(分别对应消费者1和消费者2)分别监听消息队列direct.queue1direct.queue2

      注意:

      • @RabbitListener监听注解中,利用了该注解中的bindings方法进行了队列的声明创建、交换机的声明创建、两者之间的绑定关系、BindingKey值的设置

      • 除利用@RabbitListener注解中的bindings方法外,还可以使用配置类声明Bean的方式,可详见方式二

      java 复制代码
      package at.guigu.mq.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;
      
      /**
       * 消费者消费消息(包含队列创建、绑定及监听)
       */
      @Component
      public class SpringRabbitListener {
      
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "direct.queue1"), // 队列direct.queue1
                  exchange = @Exchange(name = "cgrs572.direct", type = ExchangeTypes.DIRECT), // 交换机名称及类型
                  key = {"red", "blue"} // BindingKey
          ))
          public void listenDirectQueue1(String msg) {
              System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
          }
      
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "direct.queue2"), // 队列direct.queue2
                  exchange = @Exchange(name = "cgrs572.direct", type = ExchangeTypes.DIRECT), // 交换机名称及类型
                  key = {"red", "yellow"} // BindingKey
          ))
          public void listenDirectQueue2(String msg) {
              System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
          }
      }
    • Step2: 启动消费者模块(即consumer模块)的启动类即可。

  • 消费者(Consumer)代码步骤及示例---方式二

    • Step1: 在消费者模块(即consumer模块)的config包下创建配置类DirectConfig,然后在该类中声明交换机、队列、两者之间绑定关系的BeanBindingKey

      java 复制代码
      package at.guigu.mq.config;
      
      import org.springframework.amqp.core.*;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration
      public class DirectConfig {
          /**
           * 声明DirectExchange交换机---cgrs572.direct
           * 参数name:指定Direct交换机名称
           * @return 返回一个Direct Exchange交换机(路由机制)
           */
          @Bean
          public DirectExchange directExchange(){
              return new DirectExchange("cgrs572.direct");
          }
      
          /**
           * 声明消息队列1---direct.queue1
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue directQueue1(){
              return new Queue("direct.queue1");
          }
      
          /**
           * 将消息队列direct.queue1绑定到指定的交换机cgrs572.direct上,并设置BindingKey值---此处绑定到了Direct交换机
           * @param directQueue1 要绑定的队列
           * @param directExchange 要绑定队列的交换机
           * @return
           */
          @Bean
          public Binding directBinding1Red(Queue directQueue1, DirectExchange directExchange){
              return BindingBuilder
                      .bind(directQueue1)
                      .to(directExchange)
                      .with("red");
          }
      
          /**
           * 将消息队列direct.queue1绑定到指定的交换机cgrs572.direct上,并设置BindingKey值---此处绑定到了Direct交换机
           * @param directQueue1 要绑定的队列
           * @param directExchange 要绑定队列的交换机
           * @return
           */
          @Bean
          public Binding directBinding1Blue(Queue directQueue1, DirectExchange directExchange){
              return BindingBuilder
                      .bind(directQueue1)
                      .to(directExchange)
                      .with("blue");
          }
      
      
          /**
           * 声明消息队列2---direct.queue2
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue directQueue2(){
              return new Queue("direct.queue2");
          }
      
          /**
           * 将消息队列fanout.queue2绑定到指定的交换机cgrs572.direct,并设置BindingKey值---此处绑定到了Direct交换机
           * @param directQueue2 要绑定的队列
           * @param directExchange 要绑定队列的交换机
           * @return
           */
          @Bean
          public Binding fanoutBinding2Red(Queue directQueue2, DirectExchange directExchange){
              return BindingBuilder
                      .bind(directQueue2)
                      .to(directExchange)
                      .with("red");
          }
      
          /**
           * 将消息队列fanout.queue2绑定到指定的交换机cgrs572.direct,并设置BindingKey值---此处绑定到了Direct交换机
           * @param directQueue2 要绑定的队列
           * @param directExchange 要绑定队列的交换机
           * @return
           */
          @Bean
          public Binding fanoutBinding2Yellow(Queue directQueue2, DirectExchange directExchange){
              return BindingBuilder
                      .bind(directQueue2)
                      .to(directExchange)
                      .with("yellow");
          }
      }

      以上代码过于冗余,因此可将该配置类简化为如下代码形式

      java 复制代码
      package at.guigu.mq.config;
      
      import org.springframework.amqp.core.*;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration
      public class DirectConfig {
          /**
           * 声明DirectExchange交换机---cgrs572.direct
           * 参数name:指定Direct交换机名称
           * @return 返回一个Direct Exchange交换机(路由机制)
           */
          @Bean
          public DirectExchange directExchange(){
              return new DirectExchange("cgrs572.direct");
          }
      
          /**
           * 声明消息队列1---direct.queue1
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue directQueue1(){
              return new Queue("direct.queue1");
          }
      
          /**
           * 声明消息队列2---direct.queue2
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue directQueue2(){
              return new Queue("direct.queue2");
          }
      
          // 批量绑定
          @Bean
          public Declarables bindings() {
              return new Declarables(
                      BindingBuilder.bind(directQueue1()).to(directExchange()).with("red"),
                      BindingBuilder.bind(directQueue1()).to(directExchange()).with("blue"),
                      BindingBuilder.bind(directQueue2()).to(directExchange()).with("red"),
                      BindingBuilder.bind(directQueue2()).to(directExchange()).with("yellow")
              );
          }
      }
    • Step2: 在消费者模块(即consumer模块)的监听器listener包下创建一个监听器类SpringRabbitListener,并在该类中添加两个方法listenDirectQueue1listenDirectQueue2(分别对应消费者1和消费者2)分别监听消息队列direct.queue1direct.queue2

      java 复制代码
      package at.guigu.mq.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;
      
      /**
       * 消费者消费消息
       */
      @Component
      public class SpringRabbitListener {
      
          @RabbitListener(queues = "direct.queue1")
          public void listenDirectQueue1(String msg) {
              System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
          }
      
          @RabbitListener(queues = "direct.queue2")
          public void listenDirectQueue2(String msg) {
              System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
          }
      }
    • Step3: 启动消费者模块(即consumer模块)的启动类,打开RabbitMQ管理控制台,判断是否队列和交换机是否绑定成功。如图一所示,两个消息队列创建成功;如图二所示,交换机创建成功;如图三所示,声明的两个队列均与交换机绑定成功。

  • 生产者(Publisher)代码步骤及示例

    • Step1: 在消费者模块(即consumer模块)的test包下创建测试类SpringAmqpDirectExchangePubTest,代码如下

      java 复制代码
      package at.guigu.mq;
      
      import org.junit.jupiter.api.Test;
      import org.junit.runner.RunWith;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      
      /**
       * Fanout Exchange交换机 生产者代码示例
       */
      @RunWith(SpringRunner.class)
      @SpringBootTest
      class SpringAmqpDirectExchangePubTest {
          @Autowired
          private RabbitTemplate rabbitTemplate; // RabbitTemplate用于发送和接收消息
          @Test
          void testFanoutExchange() throws InterruptedException {
              String exchangeName = "cgrs572.direct"; // 交换机名称
              String message = "Hello, Direct_Exchange_BLUE!!!"; // 消息
              rabbitTemplate.convertAndSend(exchangeName, "blue", message); // 将消息通过交换机路由到RoutingKey=BindingKey的消息队列
          }
      }
    • Step2: 运行该测试类,在消费者模块(即consumer模块)的控制台即可看到,生产者将消息投递到交换机后,交换机会将消息发送给RoutingKey=BindingKey的消息队列中

    注意:由于队列direct.queue1BindingKey中存在blue,而direct.queue2中并不存在,所以direct.queue2并不会接收到消息,若想要两个队列都接收到消息,则需将RoutingKey设置为两个队列共有的BindingKey值,代码及运行示例如下图所示

Fanout Exchange(广播交换机)示例

  • SpringAMQP提供了声明交换机、队列以及绑定关系的API

    • 交换机API

    • 队列API:Queue

    • 绑定关系API:Binding

示例:

​ 1.在消费者模块(即consumer模块)中利用代码声明交换机、队列,并将两者绑定

​ 2.在消费者模块(即consumer模块)中编写两个消费者方法,分别监听消息队列fanout.queue1和fanout.queue2

​ 3.在生产者模块(即publisher模块)中定义测试方法,向交换机cgrs572.fanout中发送消息
该示例Demo可详见Gitee分支FaBuDingYue/FanoutExchange的提交记录

  • 公共步骤

    • Step1: 在父工程中引入依赖:spring-amqp

      xml 复制代码
      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
    • Step2: 在生产者模块(即publisher模块)和消费者模块(即consumer模块)的配置文件application.yml中添加mq连接信息

      yml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.10.100 # rabbitMQ的ip地址
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
  • 消费者(Consumer)代码步骤及示例---方式一

    • Step1: 在消费者模块(即consumer模块)的监听器listener包下创建一个监听器类SpringRabbitListener,并在该类中添加两个方法listenFanoutQueue1listenFanoutQueue2(分别对应消费者1和消费者2)分别监听消息队列fanout.queue1fanout.queue2

      注意:

      • @RabbitListener监听注解中,利用了该注解中的bindings方法进行了队列的声明创建、交换机的声明创建、两者之间的绑定关系

      • 除利用@RabbitListener注解中的bindings方法外,还可以使用配置类声明Bean的方式,可详见方式二

      java 复制代码
      package at.guigu.mq.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;
      
      /**
       * 消费者消费消息
       */
      @Component
      public class SpringRabbitListener {
      
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "fanout.queue1"), // 队列fanout.queue1
                  exchange = @Exchange(name = "cgrs572.fanout", type = ExchangeTypes.FANOUT) // 交换机类型及名称
          ))
          public void listenFanoutQueue1(String msg) {
              System.out.println("消费者1接收到fanout.queue1的消息:【" + msg + "】");
          }
      
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "fanout.queue2"), // 队列fanout.queue1
                  exchange = @Exchange(name = "cgrs572.fanout", type = ExchangeTypes.FANOUT) // 交换机类型及名称
          ))
          public void listenFanoutQueue2(String msg) {
              System.out.println("消费者2接收到fanout.queue2的消息:【" + msg + "】");
          }
      }
    • Step2: 启动消费者模块(即consumer模块)的启动类即可。

  • 消费者(Consumer)代码步骤及示例---方式二

    • Step1: 在消费者模块(即consumer模块)的config包下创建配置类FanoutConfig,然后在该类中声明交换机、队列、两者之间绑定关系的Bean

      java 复制代码
      package at.guigu.mq.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;
      
      @Configuration
      public class FanoutConfig {
          /**
           * 声明FanoutExchange交换机---cgrs572.fanout
           * 参数name:指定FanOut交换机名称
           * @return 返回一个FanOut Exchange交换机(广播机制)
           */
          @Bean
          public FanoutExchange fanoutExchange(){
              return new FanoutExchange("cgrs572.fanout");
          }
      
      
          /**
           * 声明消息队列1---fanout.queue1
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue fanoutQueue1(){
              return new Queue("fanout.queue1");
          }
      
          /**
           * 将消息队列fanout.queue1绑定到指定的交换机cgrs572.fanout上---此处绑定到了FanOut交换机
           * @param fanoutQueue1 要绑定的队列
           * @param fanoutExchange 要绑定队列的交换机
           * @return
           */
          @Bean
          public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
              return BindingBuilder
                      .bind(fanoutQueue1)
                      .to(fanoutExchange);
          }
      
          /**
           * 声明消息队列2---cgrs572.queue2
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue fanoutQueue2(){
              return new Queue("fanout.queue2");
          }
      
          /**
           * 将消息队列fanout.queue2绑定到指定的交换机cgrs572.fanout上---此处绑定到了FanOut交换机
           * @param fanoutQueue2 要绑定的队列
           * @param fanoutExchange 要绑定队列的交换机
           * @return
           */
          @Bean
          public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
              return BindingBuilder
                      .bind(fanoutQueue2)
                      .to(fanoutExchange);
          }
      }

      以上代码过于冗余,因此可将该配置类简化为如下代码形式

      java 复制代码
      package at.guigu.mq.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;
      
      @Configuration
      public class FanoutConfig {
          /**
           * 声明FanoutExchange交换机---cgrs572.fanout
           * 参数name:指定FanOut交换机名称
           * @return 返回一个FanOut Exchange交换机(广播机制)
           */
          @Bean
          public FanoutExchange fanoutExchange(){
              return new FanoutExchange("cgrs572.fanout");
          }
      
          /**
           * 声明消息队列1---fanout.queue1
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue fanoutQueue1(){
              return new Queue("fanout.queue1");
          }
      
          /**
           * 声明消息队列2---cgrs572.queue2
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue fanoutQueue2(){
              return new Queue("fanout.queue2");
          }
          
          // 批量绑定
          @Bean
          public Declarables bindings() {
              return new Declarables(
                      BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange()),
                      BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange())
              );
          }
      }
    • Step2: 在消费者模块(即consumer模块)的监听器listener包下创建一个监听器类SpringRabbitListener,并在该类中添加两个方法listenFanoutQueue1listenFanoutQueue2(分别对应消费者1和消费者2)分别监听消息队列fanout.queue1fanout.queue2

      java 复制代码
      package at.guigu.mq.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.Map;
      
      @Component
      public class SpringRabbitListener {
      
          @RabbitListener(queues = "fanout.queue1")
          public void listenFanoutQueue1(String msg) {
              System.out.println("消费者1接收到fanout.queue1的消息:【" + msg + "】");
          }
      
          @RabbitListener(queues = "fanout.queue2")
          public void listenFanoutQueue2(String msg) {
              System.out.println("消费者2接收到fanout.queue2的消息:【" + msg + "】");
          }
      }
    • Step3: 启动消费者模块(即consumer模块)的启动类,打开RabbitMQ管理控制台,判断是否队列和交换机是否绑定成功。如图一所示,两个消息队列创建成功;如图二所示,交换机创建成功;如图三所示,声明的两个队列均与交换机绑定成功。

  • 生产者(Publisher)代码步骤及示例

    • Step1: 在消费者模块(即consumer模块)的test包下创建测试类SpringAmqpFanoutExchangePubTest,代码如下:

      java 复制代码
      package at.guigu.mq;
      
      import org.junit.jupiter.api.Test;
      import org.junit.runner.RunWith;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      
      /**
       * Fanout Exchange交换机 生产者代码示例
       */
      @RunWith(SpringRunner.class)
      @SpringBootTest
      class SpringAmqpFanoutExchangePubTest {
          @Autowired
          private RabbitTemplate rabbitTemplate; // RabbitTemplate用于发送和接收消息
          @Test
          void testFanoutExchange() throws InterruptedException {
              String exchangeName = "cgrs572.fanout"; // 交换机名称
              String message = "Hello, Fanout_Exchange!!!"; // 消息
              rabbitTemplate.convertAndSend(exchangeName, "", message); // 将消息投递到交换机
              }
      }
    • Step2: 运行该测试类,在消费者模块(即consumer模块)的控制台即可看到,生产者将消息投递到交换机后,交换机会将消息发送给绑定到该交换机的两个消息队列中,然后监听这两个消息队列的消费者1和消费者2即可接收到消息。

Topic Exchange(主题交换机)示例

该示例Demo可详见Gitee分支FaBuDingYue/TopicExchange的提交记录
示例:

​ 1.在消费者模块(即consumer模块)中利用代码声明交换机、队列,并将两者绑定

​ 2.在消费者模块(即consumer模块)中编写两个消费者方法,分别监听消息队列topic.queue1和topic.queue2

​ 3.在生产者模块(即publisher模块)中定义测试方法,向交换机cgrs572.topic中发送消息

  • 公共步骤

    • Step1: 在父工程中引入依赖:spring-amqp

      xml 复制代码
      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
    • Step2: 在生产者模块(即publisher模块)和消费者模块(即consumer模块)的配置文件application.yml中添加mq连接信息

      yml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.10.100 # rabbitMQ的ip地址
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
  • 消费者(Consumer)代码步骤及示例---方式一

    • Step1: 在消费者模块(即consumer模块)的监听器listener包下创建一个监听器类SpringRabbitListener,并在该类中添加两个方法listenTopicQueue1listenTopicQueue2(分别对应消费者1和消费者2)分别监听消息队列topic.queue1topic.queue2

      注意:

      • @RabbitListener监听注解中,利用了该注解中的bindings方法进行了队列的声明创建、交换机的声明创建、两者之间的绑定关系、BindingKey值的设置

      • 除利用@RabbitListener注解中的bindings方法外,还可以使用配置类声明Bean的方式,可详见方式二

      java 复制代码
      package at.guigu.mq.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;
      
      /**
       * 消费者消费消息
       */
      @Component
      public class SpringRabbitListener {
      
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "topic.queue1"), // 队列topic.queue1
                  exchange = @Exchange(name = "cgrs572.topic", type = ExchangeTypes.TOPIC), // 交换机名称及类型
                  key = "china.#" // BindingKey
          ))
          public void listenTopicQueue1(String msg) {
              System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
          }
      
          @RabbitListener(bindings = @QueueBinding(
                  value = @Queue(name = "topic.queue2"), // 队列topic.queue2
                  exchange = @Exchange(name = "cgrs572.topic", type = ExchangeTypes.TOPIC), // 交换机名称及类型
                  key = "#.news" // BindingKey
          ))
          public void listenTopicQueue2(String msg) {
              System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
          }
      }
    • Step2: 启动消费者模块(即consumer模块)的启动类即可。

  • 消费者(Consumer)代码步骤及示例---方式二

    • Step1: 在消费者模块(即consumer模块)的config包下创建配置类TopicConfig,然后在该类中声明交换机、队列、两者之间绑定关系的BeanBindingKey

      java 复制代码
      package at.guigu.mq.config;
      
      import org.springframework.amqp.core.*;
      import org.springframework.context.annotation.Bean;
      
      public class TopicConfig {
          /**
           * 声明TopicExchange交换机---cgrs572.topic
           * 参数name:指定Topic交换机名称
           * @return 返回一个Topic Exchange交换机(主题交换机)
           */
          @Bean
          public TopicExchange topicExchange(){
              return new TopicExchange("cgrs572.topic");
          }
      
          /**
           * 声明消息队列1---topic.queue1
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue topicQueue1(){
              return new Queue("topic.queue1");
          }
      
          /**
           * 将消息队列topic.queue1绑定到指定的交换机cgrs572.topic上---此处绑定到了Topic交换机
           * @param topicQueue1 要绑定的队列
           * @param topicExchange 要绑定队列的交换机
           * @return
           */
          @Bean
          public Binding topicBinding1(Queue topicQueue1, TopicExchange topicExchange){
              return BindingBuilder
                      .bind(topicQueue1)
                      .to(topicExchange)
                      .with("china.#");
          }
      
          /**
           * 声明消息队列2---cgrs572.topic
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue topicQueue2(){
              return new Queue("topic.queue2");
          }
      
          /**
           * 将消息队列topic.queue2绑定到指定的交换机cgrs572.topic上---此处绑定到了Topic交换机
           * @param topicQueue2 要绑定的队列
           * @param topicExchange 要绑定队列的交换机
           * @return
           */
          @Bean
          public Binding topicBinding2(Queue topicQueue2, TopicExchange topicExchange){
              return BindingBuilder
                      .bind(topicQueue2)
                      .to(topicExchange)
                      .with("#.news");
          }
      }

      以上代码过于冗余,因此可将该配置类简化为如下代码形式

      java 复制代码
      package at.guigu.mq.config;
      
      import org.springframework.amqp.core.*;
      import org.springframework.context.annotation.Bean;
      
      public class TopicConfig {
          /**
           * 声明TopicExchange交换机---cgrs572.topic
           * 参数name:指定Topic交换机名称
           * @return 返回一个Topic Exchange交换机(主题交换机)
           */
          @Bean
          public TopicExchange topicExchange(){
              return new TopicExchange("cgrs572.topic");
          }
      
          /**
           * 声明消息队列1---topic.queue1
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue topicQueue1(){
              return new Queue("topic.queue1");
          }
      
          /**
           * 声明消息队列2---cgrs572.topic
           * 参数name:指定要创建的消息队列名称
           * @return 返回指定名称的消息队列
           */
          @Bean
          public Queue topicQueue2(){
              return new Queue("topic.queue2");
          }
      
          // 批量绑定
          @Bean
          public Declarables bindings() {
              return new Declarables(
                      BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("china.#"),
                      BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("#.news")
              );
          }
      }
    • Step2: 在消费者模块(即consumer模块)的监听器listener包下创建一个监听器类SpringRabbitListener,并在该类中添加两个方法listenTopicQueue1listenTopicQueue2(分别对应消费者1和消费者2)分别监听消息队列topic.queue1topic.queue2

      java 复制代码
      package at.guigu.mq.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;
      
      /**
       * 消费者消费消息
       */
      @Component
      public class SpringRabbitListener {
      
          @RabbitListener(queues = "topic.queue1")
          public void listenTopicQueue1(String msg) {
              System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
          }
      
          @RabbitListener(queues = "topic.queue2")
          public void listenTopicQueue2(String msg) {
              System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
          }
      }
    • Step3: 启动消费者模块(即consumer模块)的启动类,打开RabbitMQ管理控制台,判断是否队列和交换机是否绑定成功。如图一所示,两个消息队列创建成功;如图二所示,交换机创建成功;如图三所示,声明的两个队列均与交换机绑定成功。

  • 生产者(Publisher)代码步骤及示例

    • Step1: 在消费者模块(即consumer模块)的test包下创建测试类SpringAmqpTopicExchangePubTest,代码如下:

      java 复制代码
      package at.guigu.mq;
      
      import org.junit.jupiter.api.Test;
      import org.junit.runner.RunWith;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      
      /**
       * 生产者消息发送
       */
      @RunWith(SpringRunner.class)
      @SpringBootTest
      class SpringAmqpTopicExchangePubTest {
          @Autowired
          private RabbitTemplate rabbitTemplate; // RabbitTemplate用于发送和接收消息
          @Test
          void testSimpleQueue() {
              String exchangeName = "cgrs572.topic"; // 交换机名称
              String message = "Hello, Topic_Exchange!!!"; // 消息
              rabbitTemplate.convertAndSend(exchangeName, "china.worker", message); // 将消息通过交换机路由到RoutingKey符合BindingKey表达式的消息队列
          }
      }
    • Step2: 运行该测试类,在消费者模块(即consumer模块)的控制台即可看到,生产者将消息投递到交换机后,交换机会将消息发送给符合匹配规则的消息队列中,然后监听消息队列的消费者即可接收到消息。

      注意:由于队列topic.queue1BindingKey匹配的是以china.开头的RoutingKey,所以可接收到消息;而队列topic.queue2BindingKey匹配的是以.news结尾的RoutingKey,所以该队列无法接收到消息,若想要该队列也接收到消息则需将RoutingKey的值设置为符合两个队列BindingKey匹配规则的形式,代码及运行示例如下图所示

相关推荐
Bug退退退1233 小时前
Java 网络流式编程
java·服务器·spring·sse
小杨的全栈之路3 小时前
冒泡、插入、选择、归并、堆排序:从名字由来到Java实现,一篇讲透
java·排序算法
yinke小琪3 小时前
面试官:谈谈为什么要拆分数据库?有哪些方法?
java·后端·面试
自由的疯3 小时前
java DWG文件转图片
java·后端·架构
小兔崽子去哪了3 小时前
EasyExcel 使用
java·excel
青云交3 小时前
Java 大视界 -- Java 大数据机器学习模型的对抗攻击与防御技术研究
java·机器学习模型·对抗攻击·java 大数据·防御技术·对抗训练·i - fgsm
程序员小假3 小时前
请介绍类加载过程,什么是双亲委派模型?
java·后端
汤姆yu3 小时前
基于springboot的家具商城销售系统
java·spring boot·后端
红尘客栈23 小时前
K8s-kubeadmin 1.28安装
java·网络·kubernetes