瑞_RabbitMQ_Java客户端SpringAMQP

文章目录

    • [1 初识MQ](#1 初识MQ)
    • [2 RabbitMQ](#2 RabbitMQ)
      • [2.1 安装](#2.1 安装)
        • [2.1.1 资源准备](#2.1.1 资源准备)
        • [2.1.2 安装步骤](#2.1.2 安装步骤)
    • [3 Java客户端SpringAMQP](#3 Java客户端SpringAMQP)
      • [3.1 导入Demo工程(含资源)](#3.1 导入Demo工程(含资源))
      • [3.2 快速入门](#3.2 快速入门)
        • [3.2.0 准备工作](#3.2.0 准备工作)
        • [3.2.1 消息发送 publisher](#3.2.1 消息发送 publisher)
        • [3.2.2 消息接收 consumer](#3.2.2 消息接收 consumer)
        • [3.2.3 测试](#3.2.3 测试)
      • [3.3 WorkQueues模型](#3.3 WorkQueues模型)
        • [3.3.0 准备](#3.3.0 准备)
        • [3.3.1 消息发送 publisher](#3.3.1 消息发送 publisher)
        • [3.3.2 消息接收 consumer](#3.3.2 消息接收 consumer)
        • [3.3.3 测试-消息堆积](#3.3.3 测试-消息堆积)
        • [3.3.4 能者多劳-消费者消息推送限制★](#3.3.4 能者多劳-消费者消息推送限制★)
        • [3.3.5 总结](#3.3.5 总结)
      • [3.4 交换机类型](#3.4 交换机类型)
      • [3.5 Fanout交换机(广播模式)](#3.5 Fanout交换机(广播模式))
        • [3.5.1 声明队列和交换机](#3.5.1 声明队列和交换机)
        • [3.5.2 消息发送 publisher](#3.5.2 消息发送 publisher)
        • [3.5.3 消息接收 consumer](#3.5.3 消息接收 consumer)
        • [3.5.4 测试](#3.5.4 测试)
        • [3.5.5 总结](#3.5.5 总结)
      • [3.6 Direct交换机-定向路由](#3.6 Direct交换机-定向路由)
        • [3.6.1 声明队列和交换机](#3.6.1 声明队列和交换机)
        • [3.6.2 消息发送 publisher](#3.6.2 消息发送 publisher)
        • [3.6.3 消息接收 consumer](#3.6.3 消息接收 consumer)
        • [3.6.4 测试](#3.6.4 测试)
        • [3.6.5 总结](#3.6.5 总结)
      • [3.7 Topic交换机-通配符★](#3.7 Topic交换机-通配符★)
        • [3.7.1 声明队列和交换机](#3.7.1 声明队列和交换机)
        • [3.7.2 消息发送 publisher](#3.7.2 消息发送 publisher)
        • [3.7.3 消息接收 consumer](#3.7.3 消息接收 consumer)
        • [3.7.4 测试](#3.7.4 测试)
        • [3.7.5 总结](#3.7.5 总结)
      • [3.8 声明队列和交换机 Java代码创建★](#3.8 声明队列和交换机 Java代码创建★)
        • [3.8.1 基本API](#3.8.1 基本API)
        • [3.8.2 fanout示例](#3.8.2 fanout示例)
        • [3.8.3 direct示例](#3.8.3 direct示例)
        • [3.8.4 基于注解声明★★★](#3.8.4 基于注解声明★★★)
      • [3.9 消息转换器](#3.9 消息转换器)
        • [3.9.1 测试默认转换器](#3.9.1 测试默认转换器)
        • [3.9.2 配置JSON转换器](#3.9.2 配置JSON转换器)
        • [3.9.3 消费者接收Object](#3.9.3 消费者接收Object)
    • [4 业务改造](#4 业务改造)
      • [4.1 配置MQ](#4.1 配置MQ)
      • [4.1 接收消息](#4.1 接收消息)
      • [4.2 发送消息](#4.2 发送消息)

🙊 前言:本文章为瑞_系列专栏之《RabbitMQ》的Java客户端SpringAMQP篇。由于博主是从B站黑马程序员的《RabbitMQ》学习其相关知识,所以本系列专栏主要是针对该课程进行笔记总结和拓展,文中的部分原理及图解等也是来源于黑马提供的资料,特此注明。本文仅供大家交流、学习及研究使用,禁止用于商业用途,违者必究!


1 初识MQ

瑞:具体请见《瑞_RabbitMQ_初识MQ》

MQ(MessageQueue),中文是消息队列,如同字面上的意思就是存放消息的队列。也就是异步调用中的 Broker 角色,目比较常见的 MQ 实现如下

  • ActiveMQ
  • RabbitMQ
  • RocketMQ
  • Kafka

以上 MQ 的对比

RabbitMQ ActiveMQ RocketMQ Kafka
公司/社区 Rabbit Apache 阿里 Apache
开发语言 Erlang Java Java Scala&Java
协议支持 AMQP,XMPP,SMTP,STOMP OpenWire,STOMP,REST,XMPP,AMQP 自定义协议 自定义协议
可用性 高(主从架构) 高(主从架构) 非常高(分布式架构) 非常高(分布式架构)
单机吞吐量 一般(万级) 差 (万级) 高 (十万级) 非常高(百万级)
消息延迟 微秒级(us级) 毫秒级(ms级) 毫秒级(ms级) 毫秒以内(ms级以内)
消息可靠性 一般 一般
功能特性 基于Erlang开发,所以并发能力很强,性能极其好,延时很低;管理界面较丰富 成熟的产品,在很多公司得到应用,有较多的文档;各种协议支持较好 MQ功能比较完备,扩展性佳 只支持主要的MQ功能,像一些消息查询、消息回溯等功能没有提供,是为大数据准备的,在大数据领域应用广
  • 追求可用性:Kafka、 RocketMQ 、RabbitMQ
  • 追求可靠性:RabbitMQ、RocketMQ
  • 追求吞吐能力:RocketMQ、Kafka
  • 追求消息低延迟:RabbitMQ、Kafka

RabbitMQ 是基于 Erlang 语言开发的开源消息通信中间件,性能好,Erlang 语言是面向并发的语言;协议支持丰富,符合微服务理念,Spring 官方默认支持 RabbitMQ ;支持集群,可用性高;单机吞吐量(并发能力)十万二十万左右的样子,但已经满足大多数企业级应用需求;消息延迟在毫秒级;需要消息确认,消息可靠性高。

Kafka 适用于吞吐量需求很高的场景中,如日志搜集,但由于其消息不可靠,可能存在数据丢失的情况。

据统计,大厂基本上是使用自研,而中小型企业消息队列使用最多的是 RabbitMQ,因为其各方面都比较均衡,稳定性也好。至于 RocketMQ 由于是阿里的产品,而阿里每年向外输出大量的人才,这些人才流入到中小型企业中,会优先选择去使用 RocketMQ ,但具数据统计 RabbitMQ 在国内还是更受欢迎。

瑞:除了 RabbitMQ 以外,其它的 MQ 都是大厂开发的。大厂有大厂的问题,小厂有小厂的好处。Rabbit 公司主营核心业务之一就是 RabbitMQ ,有团队专门维护,社区也活跃。而其它大厂开发的业务多,如阿里,在阿里待过的同学就知道,阿里中的开源往往是奔 KPI 去的,想要升职加薪得有业绩,得有开源贡献,完成业绩指标之后,后续的升级、维护工作可能就没有那么到位,就会导致 bug 比较多,常见的如 FastJson

2 RabbitMQ

瑞:具体请见《瑞_RabbitMQ_初识MQ》

RabbitMQ的官网地址:Messaging that just works --- RabbitMQ

2.1 安装

本文是基于 Docker 来安装 RabbitMQ

瑞:Docker 的安装和使用可以参考《瑞_Docker(笔记超详细,有这一篇就够了)》

瑞:博主使用的SSH客户端远程连接工具是 MobaXterm,有需要的小伙伴可以参考《瑞_Java所有相关环境及软件的安装和卸载》

2.1.1 资源准备

如果是内网中开发或拉取镜像困难,请准备好以下资源,如果您的设备可以连接互联网,则可以直接开始安装

瑞:下面是博主提供rabbitmq:3.8-management的 Docker 镜像文件压缩包mq.tar的某度网盘链接,有需要的伙伴请自提

bash 复制代码
链接:https://pan.baidu.com/s/1tBRud60ExkPXcOBsr7R_rA?pwd=sm4u 
提取码:sm4u

mq.tar上传至root目录下,执行以下命令加载镜像

bash 复制代码
cd /root
docker load -i mq.tar
2.1.2 安装步骤

执行以下命令进行安装

shell 复制代码
docker run \
 -e RABBITMQ_DEFAULT_USER=ray \
 -e RABBITMQ_DEFAULT_PASS=123456 \
 -v mq-plugins:/plugins \
 --name mq \
 --hostname mq \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3.8-management

【命令解读】

  • -e RABBITMQ_DEFAULT_USER=ray

    设置 RabbitMQ 用户名为:ray

  • -e RABBITMQ_DEFAULT_PASS=123456

    设置 RabbitMQ 密码为:123456

  • -v mq-plugins:/plugins

    挂载数据卷 mq-plugins 对应容器内目录 plugins

  • --name mq

    容器名为:mq

  • --hostname mq

    主机名为:mq

  • -p 15672:15672

    端口映射,表示将本机的 15672 端口映射到 RabbitMQ 镜像的 15672 端口

  • -p 5672:5672

    端口映射,表示将本机的 5672 端口映射到 RabbitMQ 镜像的 5672 端口

  • docker run -d

    创建并运行一个容器,-d则是让容器以后台进程运行

在安装命令中有两个映射的端口

  • 15672:RabbitMQ 提供的管理控制台的端口
  • 5672:RabbitMQ 的消息发送处理接口

如上图所示,安装完成后,访问 http://192.168.133.131:15672即可看到管理控制台(记得 IP 要替换为你虚拟机的 IP)

瑞:如果页面无法访问,有可能是因为你的服务器或者虚拟机的对应端口没有开放,请参考《瑞_Linux防火墙相关命令_Windows远程连接虚拟机的服务失败_Linux防火墙端口开放》将 15672 以及 5672 端口开放

首次访问需要登录,默认的用户名和密码在 docker run 中指定了,博主的用户名为:ray,密码为:123456

登录成功后即可看到管理控制台总览页面


RabbitMQ 安装完成


3 Java客户端SpringAMQP

将来我们开发业务功能的时候,肯定不会在控制台收发消息,而是基于编程的方式。由于 RabbitMQ 采用了AMQP协议,因此它具备跨语言的特性。任何语言只要遵循AMQP协议收发消息,都可以与 RabbitMQ 交互。并且 RabbitMQ 官方也提供了各种不同语言的客户端。

AMQP 全称是 A dvanced M essage Q ueuing P rotocol,译为高级消息队列协议。是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,任何语言都可以用 AMQP 来收发消息,更符合微服务中独立性的要求。

但是, RabbitMQ 官方提供的 Java 客户端编码相对复杂,一般生产环境下我们更多会结合 Spring 来使用。而 Spring 的官方刚好基于 RabbitMQ 提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。

Spring AMQP 是基于 AMQP 协议定义的一套 API 规范,提供了模版来发送和接收消息。包含两部分,其中 spring-amqp 基础抽象,spring-rabbit是底层的默认实现。

SpringAMQP 的官方地址:https://spring.io/projects/spring-amqp

SpringAMQP 提供了三个功能 ⬇️

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

本章节就是介绍如何使用SpringAMQP实现对 RabbitMQ 的消息收发。

3.1 导入Demo工程(含资源)

瑞:以下两个 Demo 工程都可用,任选一个导入即可

以下为博主提供的 Demo 工程(在黑马的资料上修改后,更适用于本博客),仅为大家方便学习 SpringAMQP 的使用,需要自取

文件:mq-demo【Java客户端SpringAMQP】.zip

链接:https://pan.baidu.com/s/1cdGgijSxBkttNge8Bnmrhw?pwd=7jng

提取码:7jng

以下为"黑马程序员"对大家提供的一个 Demo 工程,方便学习 SpringAMQP 的使用,需要自取

链接:https://pan.baidu.com/s/197UBLdDOXXmg-_8ZyXqndg?pwd=nr1i

提取码:nr1i

用 IDEA 打开,项目结构如下图

包括三部分⬇️

  • mq-demo:父工程,管理项目依赖
  • publisher:消息的发送者
  • consumer:消息的消费者

mq-demo这个父工程中,已经配置好了 SpringAMQP 相关的依赖

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itcast.demo</groupId>
    <artifactId>mq-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>publisher</module>
        <module>consumer</module>
    </modules>
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

因此,子工程中就可以直接使用 SpringAMQP

3.2 快速入门

瑞:快速入门案例是一个生产者,一个队列绑定一个消费者

《瑞_RabbitMQ_初识MQ》的案例中,都是经过交换机发送消息到队列,不过有时候为了测试方便,也可以直接向队列发送消息,跳过交换机

所以在快速入门案例中,我们就演示这样的简单模型,基本上可以认为是 SpringAMQP 的 Hello world 案例,如下图


【需求】

  • 利用控制台创建队列 simple.queue
  • 在 publisher 服务中,利用 SpringAMQP 直接向 simple.queue 发送消息
  • 在 consumer 服务中,利用 SpringAMQP 编写消费者,监听 simple.queue 队列

可以将需求简单理解为

  • publisher 直接发送消息到队列
  • 消费者监听并处理队列中的消息

⚠️ 注意:这种模式一般仅用于测试或学习,很少在生产中使用

3.2.0 准备工作

为了方便测试,我们先在控制台新建一个队列:simple.queue

控制台:访问http://192.168.133.131:15672即可看到 RabbitMQ 管理控制台(记得 IP 要替换为你虚拟机的 IP)

队列添加成功后,如下图

接下来,我们就可以利用 Java 代码收发消息了


1️⃣ 在父工程引入spring-amqp依赖,这样 pubilsher 和 consumer 服务都可以使用

bash 复制代码
        <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

2️⃣ 配置 RabbitMQ 服务端信息

在每个微服务中引入 MQ 服务端信息,让微服务与 RabbitMQ 相连接,如下为 application.yml 的 RabbitMQ 配置示例

bash 复制代码
spring:
  rabbitmq:
    # 主机名(记得改为你的虚拟机IP)
    host: 192.168.133.131
    # 端口
    port: 5672
    # RabbitMQ虚拟主机
    virtual-host: /hmall
    # 用户名
    username: hmall
    # 密码
    password: 123456

3️⃣ 发送消息,SpringAMQP 提供了RabbitTemplate工具类,方便我们发送消息。发送消息的代码示例如下

java 复制代码
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testSimpleQueue() {
        // 队列名称
        String queueName = "simple.queue";
        // 消息
        String message = "hello, spring amqp!";
        // 发送消息
        rabbitTemplate.convertAndSend(queueName, message);
    }

4️⃣ 接收消息,SpringAMQP 提供声明式的消息监听,我们只需要通过注解在方法上声明要监听的队列名称,将来 SpringAMQP 就会把消息传递给当前方法,示例代码如下

java 复制代码
@Slf4j
@Component
public class MqListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者收到了simple.queue的消息:【" + msg +"】");
        // 消息处理
        // ... 
        System.out.println("消息处理完成");
    }
}
3.2.1 消息发送 publisher

首先配置 MQ 地址,在publisher服务的application.yml中添加配置

yaml 复制代码
spring:
  rabbitmq:
    # 主机名(记得改为你的虚拟机IP)
    host: 192.168.133.131
    # 端口
    port: 5672
    # RabbitMQ虚拟主机
    virtual-host: /hmall
    # 用户名
    username: hmall
    # 密码
    password: 123456

然后在publisher服务中编写测试类SpringAmqpTest,注意测试类的包路径要和启动类包路径是一样的,不然可能会发生一些错误,并利用RabbitTemplate实现消息发送

java 复制代码
package com.itheima.publisher;

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;

import java.util.HashMap;
import java.util.Map;

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

    @Test
    void testSimpleQueue() {
        // 队列名称
        String queueName = "simple.queue";
        // 消息
        String message = "hello, spring amqp!";
        // 发送消息
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

执行testSimpleQueue方法,如下图所示则执行成功

瑞:执行测试方法时如果遇上连接超时问题Connection refused: connect,可能是你的虚拟机中没有将 15672 以及 5672 端口开放,执行以下命令开放防火墙端口

sudo firewall-cmd --zone=public --add-port=15672/tcp --permanent

sudo firewall-cmd --zone=public --add-port=5672/tcp --permanent

sudo firewall-cmd --reload

执行发送消息成功后,打开控制台,可以看到消息已经发送到队列中

控制台:访问http://192.168.133.131:15672即可看到 RabbitMQ 管理控制台(记得 IP 要替换为你虚拟机的 IP)

可以在控制台中选中队列,查看具体接收到的消息内容,如下

3.2.2 消息接收 consumer

首先配置 MQ 地址,在consumer服务的application.yml中添加配置

yaml 复制代码
spring:
  rabbitmq:
    # 主机名(记得改为你的虚拟机IP)
    host: 192.168.133.131
    # 端口
    port: 5672
    # RabbitMQ虚拟主机
    virtual-host: /hmall
    # 用户名
    username: hmall
    # 密码
    password: 123456

然后在consumer服务的com.itheima.consumer.listener包中新建一个类MqListener,代码如下

java 复制代码
package com.itheima.consumer.listeners;

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

@Component
public class MqListener {
    
    // 利用RabbitListener 来声明要监听的队列信息
    // 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。
    // 可以看到方法体中接收的就是消息体的内容
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者收到了simple.queue的消息:【" + msg +"】");
        // 消息处理
        System.out.println("消息处理完成");
    }
}

注意:consumer服务的监听是非阻塞的

3.2.3 测试

瑞:从链接下载的小伙伴,记得把MqListener中除listenSimpleQueue以外的其它方法全部注释或删除,以及 consumer 服务中的 config 包下的 DirectConfiguration 和 FanoutConfiguration 类进行注释或删除,那些是后续的内容,如果不注释或删除可能会影响测试

重启consumer服务的启动类ConsumerApplication,然后在publisher服务中运行测试方法testSimpleQueue,发送 MQ 消息。最终consumer服务中会收到消息,如下图

由于consumer服务的监听是非阻塞的,所以consumer服务可以一直启动,多次执行publisher服务的测试方法testSimpleQueue,可以往消息队列中多次添加消息并消费

3.3 WorkQueues模型

瑞:Work queues 是一个生产者,一个队列绑定多个消费者。

能者多劳参数(防消息堆积,默认轮询)spring.rabbitmq.listener.simple.prefetch: 1

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息,防止消息堆积

Work queues,任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。

此时就可以使用 Work queues 模型,多个消费者共同处理消息处理,消息处理的速度就能大大提高

接下来,我们就来模拟这样的场景,基本思路如下所示

  • 在 RabbitMQ 的控制台创建一个队列,名为 work.queue
  • 在 publisher 服务中定义测试方法,在 1 秒内产生 50 条消息,发送到 work.queue
  • 在 consumer 服务中定义两个消息监听者,都监听 work.queue 队列
  • 消费者1每秒处理 50 条消息,消费者2每秒处理 5 条消息
3.3.0 准备

1️⃣ 我们在控制台创建一个新的队列,命名为work.queue

3.3.1 消息发送 publisher

2️⃣ 这次我们循环发送,模拟大量消息堆积现象。在publisher服务中的SpringAmqpTest类中添加一个测试方法 testWorkQueue

java 复制代码
    /**
     * workQueue
     * 向队列中不停发送消息,模拟消息堆积。
     */
    @Test
    void testWorkQueue() throws InterruptedException {
        // 队列名称
        String queueName = "work.queue";
        // 消息
        String message = "hello, message_";
        for (int i = 0; i < 50; i++) {
            // 发送消息,每20毫秒发送一次,相当于每秒发送50条消息
            rabbitTemplate.convertAndSend(queueName, message + i);
            Thread.sleep(20);
        }
    }
3.3.2 消息接收 consumer

3️⃣ 要模拟多个消费者绑定同一个队列,我们在consumer服务的MqListener中添加 2 个新的方法

java 复制代码
    @RabbitListener(queues = "work.queue")
    public void listenWorkQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());
        // 相当于每秒钟处理50个消息
        Thread.sleep(20);
    }

    @RabbitListener(queues = "work.queue")
    public void listenWorkQueue2(String msg) throws InterruptedException {
        System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());
        // 相当于每秒处理5个消息
        Thread.sleep(200);
    }

注意到这两消费者,都设置了Thead.sleep,模拟任务耗时

  • 消费者1 sleep了 20 毫秒,相当于每秒钟处理50个消息
  • 消费者2 sleep了 200 毫秒,相当于每秒处理5个消息
3.3.3 测试-消息堆积

瑞:从链接下载的小伙伴,记得把consumer服务中的application.yml的配置prefetch注释或删除,以及 consumer 服务中的 config 包下的 DirectConfiguration 和 FanoutConfiguration 类进行注释或删除,那是后续的内容,如果不注释或删除可能会影响测试

4️⃣ 重启ConsumerApplication后,再执行publisher服务中刚刚编写的发送测试方法testWorkQueue。最终结果如下⬇️

java 复制代码
消费者1接收到消息:【hello, message_0】00:01:08.735
消费者2........接收到消息:【hello, message_1】00:01:08.769
消费者1接收到消息:【hello, message_2】00:01:08.802
消费者1接收到消息:【hello, message_4】00:01:08.864
消费者1接收到消息:【hello, message_6】00:01:08.932
消费者2........接收到消息:【hello, message_3】00:01:08.986
消费者1接收到消息:【hello, message_8】00:01:08.986
消费者1接收到消息:【hello, message_10】00:01:09.057
消费者1接收到消息:【hello, message_12】00:01:09.117
消费者2........接收到消息:【hello, message_5】00:01:09.186
消费者1接收到消息:【hello, message_14】00:01:09.187
消费者1接收到消息:【hello, message_16】00:01:09.249
消费者1接收到消息:【hello, message_18】00:01:09.318
消费者1接收到消息:【hello, message_20】00:01:09.382
消费者2........接收到消息:【hello, message_7】00:01:09.397
消费者1接收到消息:【hello, message_22】00:01:09.450
消费者1接收到消息:【hello, message_24】00:01:09.520
消费者1接收到消息:【hello, message_26】00:01:09.583
消费者2........接收到消息:【hello, message_9】00:01:09.598
消费者1接收到消息:【hello, message_28】00:01:09.654
消费者1接收到消息:【hello, message_30】00:01:09.715
消费者1接收到消息:【hello, message_32】00:01:09.784
消费者2........接收到消息:【hello, message_11】00:01:09.799
消费者1接收到消息:【hello, message_34】00:01:09.854
消费者1接收到消息:【hello, message_36】00:01:09.916
消费者1接收到消息:【hello, message_38】00:01:09.982
消费者2........接收到消息:【hello, message_13】00:01:10.001
消费者1接收到消息:【hello, message_40】00:01:10.032
消费者1接收到消息:【hello, message_42】00:01:10.100
消费者1接收到消息:【hello, message_44】00:01:10.168
消费者2........接收到消息:【hello, message_15】00:01:10.214
消费者1接收到消息:【hello, message_46】00:01:10.231
消费者1接收到消息:【hello, message_48】00:01:10.300
消费者2........接收到消息:【hello, message_17】00:01:10.415
消费者2........接收到消息:【hello, message_19】00:01:10.617
消费者2........接收到消息:【hello, message_21】00:01:10.818
消费者2........接收到消息:【hello, message_23】00:01:11.020
消费者2........接收到消息:【hello, message_25】00:01:11.229
消费者2........接收到消息:【hello, message_27】00:01:11.429
消费者2........接收到消息:【hello, message_29】00:01:11.632
消费者2........接收到消息:【hello, message_31】00:01:11.852
消费者2........接收到消息:【hello, message_33】00:01:12.054
消费者2........接收到消息:【hello, message_35】00:01:12.264
消费者2........接收到消息:【hello, message_37】00:01:12.465
消费者2........接收到消息:【hello, message_39】00:01:12.667
消费者2........接收到消息:【hello, message_41】00:01:12.867
消费者2........接收到消息:【hello, message_43】00:01:13.080
消费者2........接收到消息:【hello, message_45】00:01:13.282
消费者2........接收到消息:【hello, message_47】00:01:13.483
消费者2........接收到消息:【hello, message_49】00:01:13.685

可以看到消费者1消费者2竟然每人各消费了 25 条消息

  • 消费者1 很快完成了自己的 25 条消息
  • 消费者2 却在缓慢的处理自己的 25 条消息

也就是说消息默认情况下是平均分配给每个消费者(轮询),并没有考虑到消费者的处理能力。导致1个消费者空闲,另一个消费者忙的不可开交。没有充分利用每一个消费者的能力,最终消息处理的耗时远远超过了1秒。这样显然是有问题的,容易造成消息堆积。

3.3.4 能者多劳-消费者消息推送限制★

【消费者消息推送限制】

默认情况下,RabbitMQ 会将消息依次轮询投递给绑定在队列上的每一个消费者。但这并没有考虑到消费者是否已经处理完消息,可能出现消息堆积。

在 spring 中有一个简单的配置,可以解决这个问题,修改consumer服务中的application.yml,添加配置prefetch值为 1,确保同一时刻最多投递给消费者 1 条消息

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息,防止消息堆积

再次测试,重启ConsumerApplication后,再执行publisher服务中的测试方法testWorkQueue。发现结果如下⬇️

java 复制代码
消费者2........接收到消息:【hello, message_0】00:11:27.884
消费者1接收到消息:【hello, message_1】00:11:27.887
消费者1接收到消息:【hello, message_2】00:11:27.923
消费者1接收到消息:【hello, message_3】00:11:27.955
消费者1接收到消息:【hello, message_4】00:11:27.980
消费者1接收到消息:【hello, message_5】00:11:28.013
消费者1接收到消息:【hello, message_6】00:11:28.042
消费者1接收到消息:【hello, message_7】00:11:28.077
消费者1接收到消息:【hello, message_8】00:11:28.106
消费者2........接收到消息:【hello, message_9】00:11:28.139
消费者1接收到消息:【hello, message_10】00:11:28.174
消费者1接收到消息:【hello, message_11】00:11:28.202
消费者1接收到消息:【hello, message_12】00:11:28.234
消费者1接收到消息:【hello, message_13】00:11:28.266
消费者1接收到消息:【hello, message_14】00:11:28.296
消费者1接收到消息:【hello, message_15】00:11:28.328
消费者1接收到消息:【hello, message_16】00:11:28.359
消费者2........接收到消息:【hello, message_17】00:11:28.382
消费者1接收到消息:【hello, message_18】00:11:28.416
消费者1接收到消息:【hello, message_19】00:11:28.445
消费者1接收到消息:【hello, message_20】00:11:28.475
消费者1接收到消息:【hello, message_21】00:11:28.500
消费者1接收到消息:【hello, message_22】00:11:28.532
消费者1接收到消息:【hello, message_23】00:11:28.559
消费者1接收到消息:【hello, message_24】00:11:28.585
消费者2........接收到消息:【hello, message_25】00:11:28.617
消费者1接收到消息:【hello, message_26】00:11:28.646
消费者1接收到消息:【hello, message_27】00:11:28.678
消费者1接收到消息:【hello, message_28】00:11:28.713
消费者1接收到消息:【hello, message_29】00:11:28.747
消费者1接收到消息:【hello, message_30】00:11:28.777
消费者1接收到消息:【hello, message_31】00:11:28.808
消费者1接收到消息:【hello, message_32】00:11:28.841
消费者2........接收到消息:【hello, message_33】00:11:28.868
消费者1接收到消息:【hello, message_34】00:11:28.900
消费者1接收到消息:【hello, message_35】00:11:28.930
消费者1接收到消息:【hello, message_36】00:11:28.954
消费者1接收到消息:【hello, message_37】00:11:28.987
消费者1接收到消息:【hello, message_38】00:11:29.016
消费者1接收到消息:【hello, message_39】00:11:29.050
消费者2........接收到消息:【hello, message_40】00:11:29.072
消费者1接收到消息:【hello, message_41】00:11:29.102
消费者1接收到消息:【hello, message_42】00:11:29.132
消费者1接收到消息:【hello, message_43】00:11:29.156
消费者1接收到消息:【hello, message_44】00:11:29.181
消费者1接收到消息:【hello, message_45】00:11:29.213
消费者1接收到消息:【hello, message_46】00:11:29.244
消费者1接收到消息:【hello, message_47】00:11:29.276
消费者2........接收到消息:【hello, message_48】00:11:29.309
消费者1接收到消息:【hello, message_49】00:11:29.340

可以发现,由于消费者1处理速度较快,所以处理了 43 条消息;消费者2处理速度较慢,只处理了 7 条消息。而最终总的执行耗时也在 1 秒左右,大大提升效率。

可正所谓能者多劳,这样充分利用了每一个消费者的处理能力,可以有效避免消息积压问题。

3.3.5 总结

Work模型的使用⬇️

  • 多个消费者绑定到一个队列,可以加快消息处理速度,解决消息堆积
  • 同一条消息只会被一个消费者处理
  • 通过设置prefetch来控制消费者预取的消息数量,处理完一条再处理下一条,实现能者多劳

3.4 交换机类型

在以上小节的两个测试案例中,都没有交换机,是生产者直接发送消息到队列。但真正的生产环境都会经过exchange来发送消息,不是直接发送到队列。一旦引入交换机,消息发送的模式会有很大变化 ⬇️

可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化

  • Publisher:生产者,不再发送消息到队列中,而是发给交换机
  • Exchange:交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
  • Queue:消息队列也与以前一样,接收消息、缓存消息。不过队列一定要与交换机绑定。
  • Consumer:消费者,与以前一样,订阅队列,没有变化

Exchange(交换机)只负责转发消息,不具备存储消息的能力 ,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失❗️


交换机的类型有四种

  • Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是 Fanout 交换机
  • Direct:定向,订阅,基于 RoutingKey(路由 key)发送给订阅了消息的队列
  • Topic:话题,通配符订阅,与 Direct 类似,只不过 RoutingKey 可以使用通配符
  • Headers:头匹配,基于 MQ 的消息头匹配,用的较少。

瑞:由于第四种类型不常用,本文只讲解前三种交换机模式

3.5 Fanout交换机(广播模式)

Fanout Exchange 会将接收到的消息广播到每一个跟其绑定的 queue,也叫广播模式。 Fanout,英文翻译是扇出,在 MQ 中叫广播更合适

在广播模式下,消息发送流程是这样的⬇️

1️⃣ 可以有多个队列

2️⃣ 每个队列都要绑定到 Exchange(交换机)

3️⃣ 生产者发送的消息,只能发送到交换机

4️⃣ 交换机把消息发送给绑定过的所有队列

5️⃣ 订阅队列的消费者都能拿到消息


【案例】利用 SpringAMQP 演示 FanoutExchange 的使用

步骤如下:

  1. 在 RabbitMQ 控制台中,声明队列fanout.queue1fanout.queue2

  2. 在 RabbitMQ 控制台中,声明交换机hmall.fanout,将两个队列与其绑定

  3. 在 consumer 服务中,编写两个消费者方法,分别监听fanout.queue1fanout.queue2

  4. 在 publisher 中编写测试方法,向hmall.fanout发送消息

示意图如下⬇️

瑞:从链接下载的小伙伴,记得把consumer服务中的config包下的DirectConfigurationFanoutConfiguration类进行注释或删除,那是后续的内容,如果不注释或删除可能会影响测试

3.5.1 声明队列和交换机

1️⃣ 在控制台创建队列fanout.queue1

控制台:访问http://192.168.133.131:15672即可看到 RabbitMQ 管理控制台(记得 IP 要替换为你虚拟机的 IP)

2️⃣ 再创建一个队列fanout.queue2

3️⃣ 然后再创建一个交换机hmall.fanout,注意创建的时候选择 Type 为 fanout

4️⃣ 将两个队列绑定到交换机。鼠标单击hmall.fanout,打开Bindings进行绑定

3.5.2 消息发送 publisher

publisher服务的SpringAmqpTest类中添加测试方法

java 复制代码
    /**
     * FanoutExchange
     */
    @Test
    void testSendFanout() {
        // 交换机名称
        String exchangeName = "hmall.fanout";
        // 消息
        String message = "hello, everyone!";
        rabbitTemplate.convertAndSend(exchangeName, "", message);
    }
3.5.3 消息接收 consumer

consumer服务的MqListener中添加两个方法,作为消费者

java 复制代码
    @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1 收到了 fanout.queue1的消息:【" + msg +"】");
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String msg) throws InterruptedException {
        System.out.println("消费者2 收到了 fanout.queue2的消息:【" + msg +"】");
    }
3.5.4 测试

重启consumer服务的启动类ConsumerApplication,然后在publisher服务中运行测试方法testSendFanout,发送 MQ 消息。最终consumer服务中会收到消息,如下图

3.5.5 总结

交换机的作用是什么?

  • 接收 publisher 发送的消息
  • 将消息按照规则路由到与之绑定的队列
  • 不能缓存消息,路由失败,消息丢失
  • Fanout Exchange 的会将消息路由到每个绑定的队列

在 Fanout 模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费,比如某交换机是给支付业务使用,支付成功需要给用户发送短信通知,而支付失败不需要,那么通知服务只需要监听支付成功的消息,而消息服务需要同时监听到两种消息。这时就要用到 Direct 类型的 Exchange。

3.6 Direct交换机-定向路由

Direct Exchange 会将接收到的消息根据规则路由到指定的 Queue,因此称为定向路由

瑞:下文中 Exchange 和 Queue 之间指定 key 称为BindingKey,消费者通过订阅Routing key接收交换机发送到 Queue 的指定消息

  • 每一个 Queue 都与 Exchange 设置一个 BindingKey
  • 发布者发送消息时,指定消息的 Routing key
  • Exchange 将消息路由到 BindingKey 与消息 Routing Key 一致的队列

在 Direct 模型下

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个Routing key(路由key)
  • 消息的发送方在 向 Exchange 发送消息时,也必须指定消息的Routing key
  • Exchange 不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routing key与消息的Routing key完全一致,才会接收到消息

【案例】利用 SpringAMQP 演示 DirectExchange 的使用

步骤如下

  1. 在 RabbitMQ 控制台中,声明一个名为hmall.direct的交换机
  2. 在 RabbitMQ 控制台中,声明队列direct.queue1,绑定hmall.directRouting keybludred
  3. 在 RabbitMQ 控制台中,声明队列direct.queue2,绑定hmall.directRouting keyyellowred
  4. consumer服务中,编写两个消费者方法,分别监听direct.queue1direct.queue2
  5. publisher中编写测试方法,利用不同的Routing keyhmall.direct发送消息

案例需求如下图所示

3.6.1 声明队列和交换机

1️⃣在 RabbitMQ 控制台中声明两个队列direct.queue1direct.queue2,如下图

控制台:访问http://192.168.133.131:15672即可看到 RabbitMQ 管理控制台(记得 IP 要替换为你虚拟机的 IP)

2️⃣ 声明一个 direct 类型的交换机,命名为hmall.direct,Type 选择为 direct(默认选项)

3️⃣ 选中hmall.direct交换机,鼠标单击进入,使用redblue作为 key,绑定direct.queue1hmall.direct,由于控制台一次只能绑定一个 key,所以要绑定两次

下图使用red作为 Routing key 绑定direct.queue1hmall.direct

下图使用blue作为 Routing key 绑定direct.queue1hmall.direct

同理,使用redyellow作为 Routing key,绑定direct.queue2hmall.direct,此处步骤图略,最终结果如下

3.6.2 消息发送 publisher

publisher服务的SpringAmqpTest类中添加测试方法

java 复制代码
    /**
     * DirectExchange - routingKey:red
     */
    @Test
    void testSendDirect() {
        // 交换机名称
        String exchangeName = "hmall.direct";
        // 消息
        String msg = "红色消息,您订阅了 Routing key:red ";
        // 发送消息
        rabbitTemplate.convertAndSend(exchangeName, "red", msg);
    }
3.6.3 消息接收 consumer

consumer服务的MqListener中添加方法

java 复制代码
    @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 + "】");
    }
3.6.4 测试

重启consumer服务的启动类ConsumerApplication,然后在publisher服务中运行测试方法testSendDirect,发送 MQ 消息。由于发送的是red这个 Routing key,所以两个消费者都收到了消息,如下图


我们再切换为blue作为发送的 Routing key,在publisher服务的SpringAmqpTest类中添加测试方法

java 复制代码
    /**
     * DirectExchange - routingKey:blue
     */
    @Test
    void testSendDirect2() {
        String exchangeName = "hmall.direct";
        String msg = "蓝色消息,您订阅了 Routing key:blue ";
        rabbitTemplate.convertAndSend(exchangeName, "blue", msg);
    }

运行testSendDirect2方法,会发现,只有消费者1 收到了消息,如下图


同理,再切换为yellow作为发送的 Routing key,在publisher服务的SpringAmqpTest类中添加测试方法

java 复制代码
    /**
     * DirectExchange - routingKey:yellow
     */
    @Test
    void testSendDirect3() {
        String exchangeName = "hmall.direct";
        String msg = "黄色消息,您订阅了 Routing key:yellow ";
        rabbitTemplate.convertAndSend(exchangeName, "yellow", msg);
    }

运行testSendDirect3方法,会发现,只有消费者2 收到了消息,如下图

3.6.5 总结

Direct 交换机与 Fanout 交换机的差异

  • Fanout 交换机将消息路由给每一个与之绑定的队列
  • Direct 交换机根据 Routing Key 判断路由给哪个队列
  • 如果多个队列具有相同的 Routing Key,则与 Fanout 功能类似

3.7 Topic交换机-通配符★

瑞:Topic 做的事,其实 Direct 也能做,但 Topic 更方便灵活,推荐使用。但因为通配符逻辑复杂,所以 Topic 路由会慢一些

Topic类型的Exchange与 Direct 类似,都是可以根据Routing Key把消息路由到不同的队列

区别在于Topic类型Exchange可以让队列在绑定Routing Key的时候使用通配符,即Routing key可以是多个单词的列表,以.分割

Routing key 一般都是有一个或多个单词组成,多个单词之间以.分割,例如: item.insert

【通配符规则】

  • #:匹配一个或多个词
  • *:匹配一个,即不多不少就是 1 个词

【通配符举例】

  • item.#:能够匹配item.spu.insert 或者 item.spu
  • item.*:只能匹配item.spu

【图示】

假如此时publisher发送的消息使用的Routing Key共有四种⬇️

  • china.news 代表有中国的新闻消息
  • china.weather 代表中国的天气消息
  • japan.news 则代表日本新闻
  • japan.weather 代表日本的天气消息;

根据图示解释如下

  • topic.queue1:绑定的是china.# ,凡是以 china.开头的routing key 都会被匹配到,包括:
    • china.news
    • china.weather
  • topic.queue2:绑定的是#.news ,凡是以 .news结尾的 routing key 都会被匹配。包括:
    • china.news
    • japan.news

接下来,就按照以上所举的例子,来演示一下 Topic 交换机的用法


【案例】利用 SpringAMQP 演示 TopicExchange 的使用

步骤如下,图见图示

  1. 在 RabbitMQ 控制台中,声明队列topic.queue1topic.queue2
  2. 在 RabbitMQ 控制台中,声明交换机hmall.topic,将两个队列与其绑定
  3. consumer服务中,编写两个消费者方法,分别监听topic.queue1topic.queue2
  4. publisher中编写测试方法,利用不同的Routing Keyhmall.topic发送消息
3.7.1 声明队列和交换机

1️⃣在 RabbitMQ 控制台中声明两个队列topic.queue1topic.queue2,如下图

控制台:访问http://192.168.133.131:15672即可看到 RabbitMQ 管理控制台(记得 IP 要替换为你虚拟机的 IP)

2️⃣ 声明一个 topic 类型的交换机,命名为hmall.topic,Type 选择为 topic

3️⃣ 将两个队列利用通配符和交换机进行绑定,选中hmall.topic交换机,鼠标单击进入Exchange: hmall.topic in virtual host /hmall

下图使用china.#作为 Routing key 绑定topic.queue1hmall.topic

下图使用#.news作为 Routing key 绑定topic.queue2hmall.topic

最终结果如下

3.7.2 消息发送 publisher

publisher服务的SpringAmqpTest类中添加测试方法

java 复制代码
    /**
     * TopicExchange - routingKey:japan.news
     */
    @Test
    void testSendTopic() {
        // 交换机名称
        String exchangeName = "hmall.topic";
        // 消息
        String message = "这是一条 routingKey 为 japan.news 的消息";
        // 发送消息
        rabbitTemplate.convertAndSend(exchangeName, "japan.news", message);
    }
3.7.3 消息接收 consumer

consumer服务的MqListener中添加方法

java 复制代码
    @RabbitListener(queues = "topic.queue1")
    public void listenTopicQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1 收到了 topic.queue1的消息:【" + msg +"】");
    }

    @RabbitListener(queues = "topic.queue2")
    public void listenTopicQueue2(String msg) throws InterruptedException {
        System.out.println("消费者2 收到了 topic.queue2的消息:【" + msg +"】");
    }
3.7.4 测试

重启consumer服务的启动类ConsumerApplication,然后在publisher服务中运行测试方法testSendTopic,发送 MQ 消息。由于发送的是japan.news这个 Routing key ,而topic.queue2和交换机绑定的通配符为#.news,所以只有监听topic.queue2的消费者2 符合通配符,并收到消息,如下图


我们再切换为china.news作为发送的 Routing key,在publisher服务的SpringAmqpTest类中添加测试方法

java 复制代码
    /**
     * TopicExchange - routingKey:china.news
     */
    @Test
    void testSendTopic2() {
        String exchangeName = "hmall.topic";
        String message = "这是一条 routingKey 为 china.news 的消息";
        rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
    }

运行testSendTopic2方法,会发现,消费者1 和消费者2 都收到了消息,因为此时 Routing key 均满足通配符条件,如下图


我们再切换为china.weather作为发送的 Routing key,在publisher服务的SpringAmqpTest类中添加测试方法

bash 复制代码
    /**
     * TopicExchange - routingKey:china.weather
     */
    @Test
    void testSendTopic3() {
        String exchangeName = "hmall.topic";
        String message = "这是一条 routingKey 为 china.weather 的消息";
        rabbitTemplate.convertAndSend(exchangeName, "china.weather", message);
    }

运行testSendTopic3方法,会发现,由于发送的是china.weather这个 Routing key ,由于topic.queue1和交换机绑定的通配符为china.#对该消息感兴趣,所以只有监听topic.queue1的消费者1 符合通配符,并收到消息,如下图

3.7.5 总结

Direct 交换机与 Topic 交换机的差异

  • Topic 交换机接收的消息 Routing Key 可以是多个单词,如 **.**,以.分割
  • Topic 交换机与队列绑定时的 Binding Key 可以指定通配符
  • #:匹配一个或多个词
  • *:匹配一个,即不多不少就是 1 个词

3.8 声明队列和交换机 Java代码创建★

在以上我们都是基于 RabbitMQ 控制台来创建队列、交换机。但是在实际开发时,队列和交换机是程序员定义的,将来项目上线,又要交给运维去创建。那么程序员就需要把程序中运行的所有队列和交换机都写下来,交给运维。在这个过程中是很容易出现错误的,而且效率极低 ⚠️

因此推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建,即 Java 代码创建

3.8.1 基本API

瑞:一般由接收方或消费者(consumer)声明队列、交换机以及队列和交换机的绑定关系。而发送方或生产者(publisher)只需要关心交换机,关心往哪个交换机发送就可以了

SpringAMQP 提供了几个类,用来声明队列、交换机及其绑定关系⬇️

  • Queue:用于声明队列,可以用工厂类QueueBuilder构建
  • Exchange:用于声明交换机,可以用工厂类ExchangeBuilder构建
  • Binding:用于声明队列和交换机的绑定关系,可以用工厂类BindingBuilder构建

SpringAMQP 提供了一个Queue类(org.springframework.amqp.core.Queue ),用来创建队列

SpringAMQP 还提供了一个Exchange接口,来表示所有不同类型的交换机

我们也可以自己创建队列和交换机,不过 SpringAMQP 还提供了ExchangeBuilder来简化这个过程

而在绑定队列和交换机时,则需要使用BindingBuilder来创建Binding对象

3.8.2 fanout示例

例如,声明一个 Fanout 类型的交换机hmall.fanout2,并且创建队列fanout.queue3fanout.queue4与其绑定

一般由接收方或消费者(consumer)声明队列、交换机以及队列和交换机的绑定关系。而发送方或生产者(publisher)只需要关心交换机,关心往哪个交换机发送就可以了。所以先在consumer中创建一个包config,在该包中创建类FanoutConfiguration,如下声明队列和交换机

瑞:在引用Queue的时候要注意是引用类org.springframework.amqp.core.Queue,不要引成注解 org.springframework.amqp.rabbit.annotation.Queue

java 复制代码
package com.itheima.consumer.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 FanoutConfiguration {

    /**
     * 声明一个 FanoutExchange 交换机:hmall.fanout2
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        // 方式一:用工厂类 ExchangeBuilder 创建交换机 FanoutExchange
        // ExchangeBuilder.fanoutExchange("hmall.fanout2").build();
        // 方式二:直接创建
        return new FanoutExchange("hmall.fanout2");
    }

    /**
     * 声明第 1 个队列:fanout.queue3
     */
    @Bean
    public Queue fanoutQueue3(){
        // 方式一:用工厂类 QueueBuilder 创建队列 Queue
        // QueueBuilder.durable("fanout.queue3").build();
        // 方式二:直接创建
        return new Queue("fanout.queue3");
    }

    /**
     * 绑定方式一
     * 绑定队列 fanout.queue3 和交换机 hmall.fanout2
     */
    @Bean
    public Binding fanoutBinding3(Queue fanoutQueue3, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue3).to(fanoutExchange);
    }

    /**
     * 声明第 2 个队列:fanout.queue4
     */
    @Bean
    public Queue fanoutQueue4(){
        return new Queue("fanout.queue4");
    }

    /**
     * 绑定方式二
     * 绑定队列 fanout.queue4 和交换机 hmall.fanout2
     */
    @Bean
    public Binding fanoutBinding4(){
        return BindingBuilder.bind(fanoutQueue4()).to(fanoutExchange());
    }

}

绑定方式二,中直接调用fanoutExchange()其实会直接从 spring 容器中取,不会创建第二次,所以其实和方式一的写法是一样的

启动ConsumerApplication,在管理控制台查看效果

1️⃣ 查看队列fanout.queue3fanout.queue4的创建情况,如下

2️⃣ 查看交换机hmall.fanout2的创建情况,如下

3️⃣ 在 Exchanges 中点击 hmall.fanout2 进入其管理界面,查看交换机hmall.fanout2fanout.queue3fanout.queue4的绑定情况,如下

3.8.3 direct示例

例如,声明一个 Direct 类型的交换机hmall.direct,并且创建队列direct.queue1direct.queue2,将direct.queue1hmall.direct绑定 Routing key bule以及reddirect.queue2hmall.direct绑定 Routing key red以及yellow

由于在之前的章节中我们已经手动创建了队列direct.queue1direct.queue2以及交换机hmall.direct,其中队列和交换机的绑定关系如下

为了效果,记得要把已经创建的队列direct.queue1direct.queue2以及交换机hmall.direct全部删除

一般由接收方或消费者(consumer)声明队列、交换机以及队列和交换机的绑定关系。而发送方或生产者(publisher)只需要关心交换机,关心往哪个交换机发送就可以了。所以先在consumer中创建一个包config,在该包中创建类DirectConfiguration,如下声明队列和交换机

java 复制代码
package com.itheima.consumer.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectConfiguration {
    
    /**
     * 声明一个 DirectExchange 交换机:hmall.direct
     */
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("hmall.direct");
    }

    /**
     * 声明第 1 个队列:direct.queue1
     */
    @Bean
    public Queue directQueue1(){
        return new Queue("direct.queue1");
    }

    /**
     * 绑定队列 direct.queue1 和交换机 hmall.direct 且 routingKey 为 red
     */
    @Bean
    public Binding directQueue1BindingRed(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }

    /**
     * 绑定队列 direct.queue1 和交换机 hmall.direct 且 routingKey 为 blue
     */
    @Bean
    public Binding directQueue1BindingBlue(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }

    /**
     * 声明第 2 个队列:direct.queue2
     */
    @Bean
    public Queue directQueue2(){
        return new Queue("direct.queue2");
    }

    /**
     * 绑定队列 direct.queue2 和交换机 hmall.direct 且 routingKey 为 red
     */
    @Bean
    public Binding directQueue2BindingRed(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }

    /**
     * 绑定队列 direct.queue2 和交换机 hmall.direct 且 routingKey 为 yellow
     */
    @Bean
    public Binding directQueue2BindingYellow(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }

}

启动ConsumerApplication,在管理控制台查看效果,会创建队列direct.queue1direct.queue2以及交换机hmall.direct,且绑定关系如下

瑞:Direct 模式由于要绑定多个 KEY,会非常麻烦,每一个 routingKey 都要编写一个 binding,所以推荐使用注解

3.8.4 基于注解声明★★★

基于@Bean的方式声明队列和交换机比较麻烦, Spring 还提供了基于注解方式来声明

例如,我们同样声明 Direct 模式的交换机和队列,[点我跳转 direct 示例](#点我跳转 direct 示例)

为了效果,记得要把之前章节中已经创建的队列direct.queue1direct.queue2以及交换机hmall.direct全部删除

由于现在是使用注解的方式,所以要把之前的配置类DirectConfiguration@Configuration给注释或者直接将该配置类删除

consumer中的类中MqListener,修改listenDirectQueue1listenDirectQueue2的注解为 bindings 模式

java 复制代码
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1", durable = "true"),
            exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "blue"}
    ))
    public void listenDirectQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1 收到了 direct.queue1的消息:【" + msg +"】");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2", durable = "true"),
            exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "yellow"}
    ))
    public void listenDirectQueue2(String msg) throws InterruptedException {
        System.out.println("消费者2 收到了 direct.queue2的消息:【" + msg +"】");
    }

启动ConsumerApplication后,运行SpringAmqpTest中的testSendDirecttestSendDirect2testSendDirect3测试方法发送数据,结果如下

	消费者1 收到了 direct.queue1的消息:【红色消息,您订阅了 Routing key:red 】
	消费者2 收到了 direct.queue2的消息:【红色消息,您订阅了 Routing key:red 】
	消费者1 收到了 direct.queue1的消息:【蓝色消息,您订阅了 Routing key:blue 】
	消费者2 收到了 direct.queue2的消息:【黄色消息,您订阅了 Routing key:yellow 】

Topic 模式写法

java 复制代码
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1"),
            exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
            key = "china.#"
    ))
    public void listenTopicQueue1(String msg){
        System.out.println("消费者1 收到了 topic.queue1的消息:【" + msg +"】");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2"),
            exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
            key = "#.news"
    ))
    public void listenTopicQueue2(String msg){
        System.out.println("消费者2 收到了 topic.queue2的消息:【" + msg +"】");
    }

瑞:效果同理,具体测试及效果可参考章节:Topic交换机,点我跳转

3.9 消息转换器

在 RabbitMQ 中,消息转换器(Message Converter)用于在发送消息时将 Java 对象转换为 RabbitMQ 可以理解的字节流,以及在消费者接收消息时将字节流转换回 Java 对象

消息转换器通常用于在不同的系统或服务之间进行数据传输和交互,特别是在分布式系统中

3.9.1 测试默认转换器

测试利用 SpringAMQP 发送对象类型的消息

【测试方式一】用控制台测试(快捷)

1️⃣ 声明一个队列,名为object.queue

2️⃣ 编写单元测试,在SpringAmqpTest测试类中添加testSendObject,向队列object.queue中直接发送一条消息,消息类型为 Map,示例如下

bash 复制代码
    @Test
    void testSendObject() {
        Map<String, Object> msg = new HashMap<>(2);
        msg.put("name", "jack");
        msg.put("age", 21);
        rabbitTemplate.convertAndSend("object.queue", msg);
    }

3️⃣ 执行testSendObject方法,发送成功后在控制台查看消息,会发现content_type: application/x-java-serialized-object,而消息信息是一堆乱码


【测试方式二】用 Java 代码测试

1️⃣ 创建测试队列,在consumer服务中声明一个新的配置类MessageConfig,利用@Bean的方式创建一个队列,具体代码如下

java 复制代码
package com.itheima.consumer.config;

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MessageConfig {

    @Bean
    public Queue objectQueue() {
        return new Queue("object.queue");
    }
}

注意,这里我们先不要给这个队列添加消费者,因为我们要用控制台查看消息体的格式

重启consumer服务以后,队列就会被自动创建出来

2️⃣ 发送消息,在publisher模块的SpringAmqpTest中新增一个消息发送的方法testSendMap,发送一个 Map 对象

java 复制代码
    @Test
    public void testSendMap() {
        // 准备消息
        Map<String, Object> msg = new HashMap<>();
        msg.put("name", "阿瑞");
        msg.put("age", 21);
        // 发送消息
        rabbitTemplate.convertAndSend("object.queue", msg);
    }

运行方法testSendMap,发送消息后查看控制台,可以看到消息格式非常不友好

瑞:如果不是乱码,可能是下载的代码已经配置了MessageConverter,将其暂时注释即可
瑞:虽然我们发送的是一个对象,但 Spring 会在接收后帮我们转化为字节发送给 MQ ,因为所有的数据传输最终都是转化为字节的,具体可以看rabbitTemplate.convertAndSend("object.queue", msg);的内部实现,一直跟到org.springframework.amqp.support.converter.SimpleMessageConvertercreateMessage方法可知,最后使用的 JDK 自带的序列化对象字节流,如下图

Spring 的消息发送代码接收的消息体是一个 Object,但在数据传输时,它会把你发送的消息序列化为字节发送给 MQ,接收消息的时候,还会把字节反序列化为 Java 对象。

只不过,默认情况下 Spring 采用的序列化方式是 JDK 序列化。众所周知,JDK 序列化存在下列问题⬇️

  • 数据体积过大
  • 有安全漏洞,字节码可变,通过反序列化过程中可执行非法代码
  • 可读性差
3.9.2 配置JSON转换器

显然消息转换器默认使用 JDK 自带的序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用 JSON 方式来做序列化和反序列化。即在publisherconsumer中都配置MessageConverter,只需两步

1️⃣ 在publisherconsumer中都要引入 jackson 依赖,建议直接在父工程mq-demo中引入,这样子工程publisherconsumer就都引入了,如下

xml 复制代码
        <!--Jackson-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>

⚠️ 注意,如果项目中引入了spring-boot-starter-web依赖,则无需再次引入Jackson依赖

2️⃣ 在publisherconsumer中配置消息转换器,以下代码是在启动类中配置 Bean (也可在其它配置类中装配 Bean),注意是org.springframework.amqp.support.converter包路径下的,不要引错了

java 复制代码
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.context.annotation.Bean;

@SpringBootApplication
public class PublisherApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class);
    }

    @Bean
    public MessageConverter messageConverter(){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
}
java 复制代码
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.context.annotation.Bean;

@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

    @Bean
    public MessageConverter messageConverter(){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
}

消息转换器中添加的messageId可以便于我们将来做幂等性判断

配置完 JSON 消息转换器后,再次执行SpringAmqpTesttestSendMap消息发送的方法,到 MQ 的控制台查看消息结构,就可以看到正常的结果了

3.9.3 消费者接收Object

我们在consumer服务中定义一个新的消费者,由于publisher是用 Map 发送,那么消费者也一定要用 Map 接收(怎么发怎么收),MqListenerlistenObject方法代码如下

java 复制代码
    @RabbitListener(queues = "object.queue")
    public void listenObject(Map<String, Object> msg) throws InterruptedException {
        System.out.println("消费者 收到了 object.queue的消息:【" + msg +"】");
    }

运行ConsumerApplication,会发现第一条消息处理报错了,因为配置了 JSON 转化器后无法处理第一条 JDK 序列化的消息,而第二条消息接收正常并成功处理,如下图

理论上,第一条消息抛错了,应该没有被消费才对,但是看控制台,除掉正常消费的第二条消息,队列中应该还有一条消息才对,但实际两条消息都没了,说明第一条消息丢失,后续章节会说明解决办法

4 业务改造

瑞:本章节是基于《黑马商城》项目进行改造,可忽略

【案例需求】改造余额支付功能,将支付成功后基于 OpenFeign 的交易服务的更新订单状态接口的同步调用,改为基于 RabbitMQ 的异步通知。如图

【说明】我们只关注交易服务,步骤如下:

  • 定义topic类型交换机,命名为pay.topic
  • 定义消息队列,命名为mark.order.pay.queue
  • mark.order.pay.queuepay.topic绑定,BindingKeypay.success
  • 支付成功时不再调用交易服务更新订单状态的接口,而是发送一条消息到pay.topic,发送消息的RoutingKeypay.success,消息内容是订单id
  • 交易服务监听mark.order.pay.queue队列,接收到消息后更新订单状态为已支付

4.1 配置MQ

不管是生产者还是消费者,都需要配置 MQ 的基本信息。分为两步

1️⃣添加依赖

xml 复制代码
  <!--消息发送-->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>

2️⃣ 配置 MQ 地址

yaml 复制代码
spring:
  rabbitmq:
    # 主机名(记得改为你的虚拟机IP)
    host: 192.168.133.131
    # 端口
    port: 5672
    # RabbitMQ虚拟主机
    virtual-host: /hmall
    # 用户名
    username: hmall
    # 密码
    password: 123456

4.1 接收消息

trade-service服务中定义一个消息监听类PayStatusListener

其代码如下

java 复制代码
package com.hmall.trade.listener;

import com.hmall.trade.service.IOrderService;
import lombok.RequiredArgsConstructor;
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
@RequiredArgsConstructor
public class PayStatusListener {

    private final IOrderService orderService;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "mark.order.pay.queue", durable = "true"),
            exchange = @Exchange(name = "pay.topic", type = ExchangeTypes.TOPIC),
            key = "pay.success"
    ))
    public void listenPaySuccess(Long orderId){
        orderService.markOrderPaySuccess(orderId);
    }
}

4.2 发送消息

修改pay-service服务下的com.hmall.pay.service.impl.PayOrderServiceImpl类中的tryPayOrderByBalance方法

java 复制代码
private final RabbitTemplate rabbitTemplate;

@Override
@Transactional
public void tryPayOrderByBalance(PayOrderDTO payOrderDTO) {
    // 1.查询支付单
    PayOrder po = getById(payOrderDTO.getId());
    // 2.判断状态
    if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){
        // 订单不是未支付,状态异常
        throw new BizIllegalException("交易已支付或关闭!");
    }
    // 3.尝试扣减余额
    userClient.deductMoney(payOrderDTO.getPw(), po.getAmount());
    // 4.修改支付单状态
    boolean success = markPayOrderSuccess(payOrderDTO.getId(), LocalDateTime.now());
    if (!success) {
        throw new BizIllegalException("交易已支付或关闭!");
    }
    // 5.修改订单状态
    // tradeClient.markOrderPaySuccess(po.getBizOrderNo());
    try {
        rabbitTemplate.convertAndSend("pay.topic", "pay.success", po.getBizOrderNo());
    } catch (Exception e) {
        log.error("支付成功的消息发送失败,支付单id:{}, 交易单id:{}", po.getId(), po.getBizOrderNo(), e);
    }
}

本文是博主的粗浅理解,可能存在一些错误或不完善之处,如有遗漏或错误欢迎各位补充,谢谢

如果觉得这篇文章对您有所帮助的话,请动动小手点波关注💗,你的点赞👍收藏⭐️转发🔗评论📝都是对博主最好的支持~

相关推荐
GoodStudyAndDayDayUp8 分钟前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶38 分钟前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗1 小时前
常用类晨考day15
java
骇客野人1 小时前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言
来一杯龙舌兰1 小时前
【RabbitMQ】RabbitMQ保证消息不丢失的N种策略的思想总结
分布式·rabbitmq·ruby·持久化·ack·消息确认
yuanbenshidiaos2 小时前
c++---------数据类型
java·jvm·c++
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
Lojarro2 小时前
【Spring】Spring框架之-AOP
java·mysql·spring
莫名其妙小饼干2 小时前
网上球鞋竞拍系统|Java|SSM|VUE| 前后端分离
java·开发语言·maven·mssql
isolusion2 小时前
Springboot的创建方式
java·spring boot·后端