RabbitMQ —— How To Get Started Using RabbitMQ Quickly And Understand How It Works(如何快速上手RabbitMQ)

📖 RabbitMQ 快速掌握并上手

📑 RabbitMQ 概述

🔖 1-1 什么是消息队列 ?

什么是消息?

消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符 串,也可以更复杂,可能包含嵌入对象。

什么是消息队列?

消息队列(Message Queue)是一种应用间的通信方式消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在

🔖 1-2 为什么要使用消息队列?

从 1-1 我们可以看出来消息队列是一种应用间的异步协作机制,那什么时候需要使用 MQ 呢?

📚 异步协作 可以 用于 业务 解耦

举个🌰~ 以常见的订单系统为例

用户点击下单按钮之后的业务逻辑可能包括:扣减库存、 生成相应单据、发红包、发短信通知。在业务发展初期这些逻辑可能放在一起同步执行,随着业务的发展订单量增长,需要提升系统服务的性能,这时可以将一些不需要立即生效的操作拆分出来异步执行,比如发放红包、发短信通知等。

📚 其中加粗的业务逻辑为必要逻辑, 而其它很多非必须需立即生效的业务逻辑堆加在一起之后,业务逻辑处理的时间会非常长,我们可以将其它多个非必要立即生效的业务逻辑拆分成单独一个个的异步处理业务逻辑可以对繁杂的业务逻辑进行解耦的同时也完成了必要需立即完成的业务逻辑。

大致如图所示

在这里插入图片描述

📚 其它常见场景包括最终一致性、广播、错峰与流控等等。

最终一致性概念

简单来说,最终一致性就是两个系统的状态要么都成功、要么都失败,主要逻辑与次要逻辑可以存在一定延迟来达到最终二者状态一致即可。

广播

消息队列的基本功能之一是进行广播。如果没有消息队列,每当一个新的业务方接入,我们都要联调一次新接口。
有了消息队列,我们只需要关心消息是否送达了队列,至于谁订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。

错峰流控是什么?

当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的漏斗,在下游有能力处理的时候,再进行分发

在这里插入图片描述

🔖 1-3 RabbitMQ 是什么 ? 特点 ?

RabbitMQ 是什么?

RabbitMQ 是一个由 Erlang 语言开发的 AMQP开源实现

AMQP概述

AMQP :Advanced Message Queue Protocol,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、 开发语言等条件的限制。

⭐️ RabbitMQ 的特点

RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、 高可用性等方面表现不俗。

可靠性(Reliability)

RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认

灵活的路由(Flexible Routing)

在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。

消息集群(Clustering)

多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。

高可用(Highly Available Queues)

队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

多种协议(Multi-protocol)

RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。

多语言客户端(Many Clients)

RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。

管理界面(Management UI)

RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面

跟踪机制(Tracing)

如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。

📑 RabbitMQ 的安装

首先我们需要进入到官网下载对应的安装包

Erlang官方下载地址:https://www.erlang.org/downloads

RabbitMQ官方下载地址:https://www.rabbitmq.com/download.html

各个安装包按需自取哦~ Erlang 与 Rabbit

链接:https://pan.baidu.com/s/1Tjbm6smH9E3FLV-nKZojnw?pwd=HHXF
提取码:HHXF
–来自百度网盘超级会员V4的分享

🔖 How To Install RabbitMQ In CentOS7.6

⭐️ 安装前的准备

步骤一: 依赖包的安装(因为 RabbitMQ 是基于Erlang 写的需要安装对应编译环境的依赖等)

# 安装对应编译环境的语句
sudo yum install gcc glibc-devel make ncurses-devel openssl-devel xmlto -y

在这里插入图片描述

📚 如图显示成功~

⭐️ 安装 Erlang

步骤二:安装 Erlang

  • 首先我们需要进入到 Erlang 的官网下载对应的 tar.gz erlang源码安装包
https://www.erlang.org/patches/otp-19.3

在这里插入图片描述

  • 将我们下载好的对应的安装包上传到我们的虚拟机centOS101用户的/home/centos101/downloads 目录当中

  • 进入到 /home/centos101/downloads/ 并且将otp-19.3解压到 /usr/local/ 目录下

# 进入到对应用户目录下的 downloads 目录下
cd ~/downloads/
# 解压 otp_src_19.3.tar.gz 到指定目录 /usr/local/ 目录下
sudo tar -zxvf otp_src_19.3.tar.gz -C /usr/local
# 进入到 /usr/local/ 目录下 修改 otp_src_19.3 目录的所有权为当前用户
cd /usr/local
sudo chown -R centos101 ./otp_src_19.3
  • 创建一个用于存放 Erlang 的安装目录
# 创建 erlang 目录
sudo mkdir /usr/local/erlang
# 更改目录所有者为当前用户
sudo chown -R centos101 ./erlang
  • 进入到 Erlang 源码的解压目录 并且配置 Erlang 的安装信息
# 进入源码的解压目录
cd /usr/local/otp_src_19.3
# 配置安装信息
./configure --prefix=/usr/local/erlang --without-javac
  • 编译并安装
make && make install
  • 配置当前用户的环境变量
# 配置当前用户环境变量
vim ~/.bashrc
# 使配置的环境变量生效
source ~/.bashrc

~/.bashrc

export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_162
export ERLANG_HOME=/usr/local/erlang
export ZOOKEEPER_HOME=/usr/local/zookeeper
export CLASSPATH=.:$JAVA_HOME/lib/:$ZOOKEEPER_HOME/lib/

export PATH=$PATH:$JAVA_HOME/bin:$ZOOKEEPER_HOME/bin:$ERLANG_HOME/bin
  • 查看 Erlang 是否安装成功
[centos101@localhost otp_src_19.3]$ erl -version
Erlang (ASYNC_THREADS,HIPE) (BEAM) emulator version 8.3
⭐️ 安装 RabbitMQ

步骤三:安装RabbitMQ

  • 进入RabbitMQ 官方云仓库下载rpm安装包
https://packagecloud.io/rabbitmq/rabbitmq-server
  • 上传下载好的 rpm 到虚拟机centos101用户目录下的 downloads 目录中

  • 执行 rpm -ivh 直接进行安装~

[centos101@localhost downloads]$ sudo rpm -ivh --nodeps rabbitmq-server-3.7.2-1.el7.noarch.rpm
[sudo] centos101 的密码:
警告:rabbitmq-server-3.7.2-1.el7.noarch.rpm: 头V4 RSA/SHA1 Signature, 密钥 ID 6026dfca: NOKEY
准备中...                          ################################# [100%]
正在升级/安装...
   1:rabbitmq-server-3.7.2-1.el7      ################################# [100%]

📚 安装成功啦~

⭐️ 启动 RabbitMQ

切换到 root用户启动与关闭 RabbitMQ 服务并且开启守护进程

$ rabbitmq-server start &

📚 错误一:rabbitmq-server: line 85: erl: command not found

解决方案

配置之前要先确定好是 root 用户的环境变量已经配置好 erl rabbitmq等权限

  • 登录到 root 用户配置其 Erlang 环境变量等
vim /etc/profile
source /etc/profile
ERL_HOME=/usr/local/erlang
export PATH=$PATH:$ERL_HOME/bin

📚 错误二:/var/lib/rabbitmq/.erlang.cookie文件权限不够

错误原因是/var/lib/rabbitmq/.erlang.cookie文件权限不够。

解决方案

# 解决方案是对这个文件授权 rabbitmq:rabbitmq
chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
chmod 400 /var/lib/rabbitmq/.erlang.cookie

📚 错误三:因为当前主机的主机名 非 localhost 也会导致启动失败

解决方案

# vim /etc/hostname 中进行配置修改
⭐️ 关闭 RabbitMQ

关闭 RabbitMQ 也是需要在 root 用户权限下

[root@localhost ~]# rabbitmqctl stop
Stopping and halting node rabbit@localhost ...
Gracefully halting Erlang VM

📑 RabbitMQ 常用命令使用

🔖 3-1 插件管理

添加插件

rabbitmq-plugins enable {插件名称}

删除插件

rabbitmq-plugins disable {插件名称}

📚 注意点:

RabbitMQ 启动以后可以使用浏览器进入管控台,但是默认情况 RabbitMQ 不允许 直接使用浏览器进行访问因此必须添加插件

[root@localhost ~]# rabbitmq-plugins enable rabbitmq_management
The following plugins have been configured:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@localhost...
The following plugins have been enabled:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch

使用浏览器访问 管控台

http://192.168.56.103:15672/

在这里插入图片描述

🔖 3-2 用户管理

RabbitMQ 提供了 默认用户名与密码

账号: guest
密码: guest

📚注意点:这里的 guest 只允许本机登录访问,所以需要创建用户并且授予远程访问权限。

在这里插入图片描述

添加用户

rabbitmqctl add_user {username} {parssword}
# 创建一个用户名为 root 且密码也为root的 RabbitMQ 用户
rabbitmqctl add_user root root

删除用户

rabbitmqctl delete_user {username}

修改密码

rabbitmqctl change_password {username} {newPassword}

设置用户角色

rabbitmqctl set_user_tags {username} {tag}

📚 其中 tag 可选参数如下:

management , monitoring , policymaker ,administrator

management

用户可以通过AMQP做的任何事外加:

  • 列出自己可以通过AMQP登入的virtual hosts

  • 查看自己的virtual hosts中的queues, exchanges 和 bindings

  • 查看和关闭自己的channels 和 connections

  • 查看有关自己的virtual hosts的“全局”的统计信息,包含其他用户在这些virtual hosts中的活动。

policymaker

用户可以做management可以做的任何事外加:

查看、创建和删除自己的virtual hosts所属的policies和parameters

monitoring

用户可以做management可以做的任何事外加:

  • 列出所有virtual hosts,包括他们不能登录的virtual hosts

  • 查看其他用户的connections和channels

  • 查看节点级别的数据如clustering和memory使用情况

  • 查看真正的关于所有virtual hosts的全局的统计信息

administrator

用户可以做 policymaker和monitoring可以做的任何事外加:

  • 创建和删除virtual hosts

  • 查看、创建和删除users

  • 查看创建和删除permissions

  • 关闭其他用户的connections

在这里插入图片描述

🔖 3-3 权限分配

设置用户对命名空间路径下的哪些文件可读可写

rabbitmqctl set_permissions -p {vhostpath} {username} {conf} {write} {read}
# 设置 root 用户 拥有对以 / 路径为命名空间的目录下的 所有资源均可读可写
[root@localhost ~]# rabbitmqctl set_permissions -p / root '.*' '.*' '.*'
Setting permissions for user "root" in vhost "/" ...
  • -p vhostpath :用于指定一个资源的命名空间,例如 –p / 表示根路径命名空间

  • username:用于指定要为哪个用户授权填写用户名

  • conf:一个正则表达式match哪些配置资源能够被该用户配置。

  • write:一个正则表达式match哪些配置资源能够被该用户读.

  • read:一个正则表达式match哪些配置资源能够被该用户访问。

查看用户权限

# 查看根命名空间下用户的所有用户权限
[root@localhost ~]# rabbitmqctl list_permissions 
Listing permissions for vhost "/" ...
root	.*	.*	.*
guest	.*	.*	.*
# 查看指定命名空间目录下所有榕湖的用户权限
[root@localhost ~]# rabbitmqctl list_permissions -p {vhostpath}
Listing permissions for vhost "/" ...
root	.*	.*	.*
guest	.*	.*	.*
[root@localhost ~]# 

查看指定用户的用户权限

rabbitmqctl list_user_permissions {username}
[root@localhost ~]# rabbitmqctl list_user_permissions root
Listing permissions for user "root" ...
/	.*	.*	.*

清除用户权限

[root@localhost ~]# rabbitmqctl clear_permissions root
Clearing permissions for user "root" in vhost "/" ...

在这里插入图片描述

🔖 3-4 vhost 管理

什么是 vhost ?

vhost是RabbitMQ中的一个命名空间,可以限制消息的存放位置利用这个命名空间可以进行权限的控制有点类似Windows中的文件夹一样,在不同的文件夹中存放不同的文件。

添加一个 vhost 命名空间

[root@localhost ~]# rabbitmqctl add_vhost /alascanfu
Adding vhost "/alascanfu" ...

删除一个 vhost 命名空间

[root@localhost ~]# rabbitmqctl delete_vhost /alascanfu
Deleting vhost "/alascanfu" ...

在这里插入图片描述

📑 RabbitMQ 消息发送和接收 ⭐重点

🔖 4-1 RabbitMQ 的消息发送和接收机制

所有 MQ 产品从模型抽象上来说都是一样的过程:
消费者(consumer)订阅某个队列。生产者(producer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者。

在这里插入图片描述

📚 上面是MQ的基本抽象模型,但是不同的MQ产品有有者不同的机制,RabbitMQ实际基于AMQP协议的一个开源实现,因此RabbitMQ内部也是AMQP的基本概念。

RabbitMQ的内部接收如下:

在这里插入图片描述

Message

消息,消息是不具体的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储等。

Producer

消息的生产者,也是一个向交换器发布消息的客户端应用程序。

Exchange

交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

Binding

绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表

Queue

消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列消息一直在队列里面等待消费者连接到这个队列将其取走。

Connection

网络连接,比如一个TCP连接。

Channel

信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。

Consumer

消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

Virtual Host

虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。

Broker

表示消息队列服务器实体。

🔖 4-2 AMQP 中的消息路由

AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange 和 Binding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列

在这里插入图片描述

🔖 4-3 Exchange 类型 ⭐

⭐ Direct Exchange

见文知意,直连交换机意思是此交换机需要绑定一个队列,要求该消息与一个特定的路由键完全匹配。简单点说就是一对一的,点对点的发送。

在这里插入图片描述

Direct Exchange 大致如图所示

在这里插入图片描述

📚 小结:RabbitMQ 的交换机会根据消息中的 RoutingKey 的内容精准匹配将消息发送给与 RoutingKey 完全一致的Queue中,队列需要执行好BindingKey , 这个BindingKey 必须与消息中的 RoutingKey 完全一致,消费者只需要监听某个队列之后,就可以获取队列中的信息的信息。

⭐ Fanout Exchange

这种类型的交换机需要将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。简单点说就是发布订阅。

在这里插入图片描述

Fanout Exchange 大致如图所示

在这里插入图片描述

📚 小结:Fanout交换是一种广播模式(类似消息的发送与订阅),消息是一对多的,在这种模式中,没有 RoutingKey 和 BindingKey 的概念Bindings 只是简单的将消息与 交换机之间进行了绑定,如果消息进入了交换机当中,那么这个消息会被转发到所有与当前交换机进行绑定的所有队列中

📚 注意点 :但是上述Fanout Exchange 这种模式就和我们收看电视直播或电台直播是一样的,必须要先在消费中监听队列,就好比节目开始前先打开对应的电视调到对应的频道,否则,若消息先进性发送,那么消费者将会永远措施这部分消息。

Fanout Exchange 虽然可能会丢失信息,但是它是最快的~

⭐ Topic Exchange

直接翻译的话叫做主题交换机,如果从用法上面翻译可能叫通配符交换机会更加贴切。这种交换机是使用通配符去匹配,路由到对应的队列。通配符有两种:“*” 、 “#”。需要注意的是通配符前面必须要加上"."符号。

* 符号:有且只匹配一个词。比如 a.*可以匹配到**“a.b”、“a.c”,但是匹配不了"a.b.c",也匹配不了 a. 。**

# 符号:匹配一个或多个词。比如"rabbit.#“既可以匹配到"rabbit.a.b”、“rabbit.a”,也可以匹配到"rabbit.a.b.c"。

在这里插入图片描述

Topic Exchange 大致如图所示

在这里插入图片描述

📚 小结: Topic Exchange 与 Fanout Exchange基本概念与使用基本相同,但是 Topic Exchange 需要指定BindingKey,消息中也需要携带RoutingKey,不过消息中携带的 RoutingKey 是可以使用通配符的,*表示有且仅只有一个单词时进行匹配,#表示可以匹配0个或者任意多个单词,单词和单词之间需要用 . 来进行分割 ,它也会丢失消息,因此也应该先启动消费者来监听队列。

⭐ Headers Exchange

这种交换机用的相对没这么多。它跟上面三种有点区别,它的路由不是用routingKey进行路由匹配,而是在匹配请求头中所带的键值进行路由

创建队列需要设置绑定的头部信息,有两种模式:全部匹配和部分匹配交换机会根据生产者发送过来的头部信息携带的键值去匹配队列绑定的键值,路由到对应的队列。

📑 Java 与 RabbitMQ

🔖 5-1 Java 发送和接收 Queue 的消息

步骤一:创建好一个 Java Project 并且创建好 RabbitMQAndJava-rabbitmq-send-java 工程Module并且导入好相关依赖

pom.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>com.alascanfu</groupId>
    <artifactId>RabbitMQAndJava</artifactId>
    <version>1.0.0</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.build.outputEncoding>UTF-8</project.build.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <java.version>1.8</java.version>
        <rabbitmq.version>5.1.1</rabbitmq.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>${rabbitmq.version}</version>
        </dependency>
    </dependencies>
</project>

步骤二:编写消息发送类

com.alascanfu.rabbitmq.queue.Send

/***
 * @author: Alascanfu
 * @date : Created in 2022/5/28 0:29
 * @description: 消息发送类
 * @modified By: Alascanfu
 **/
public class Send {
    static final String RABBITMQ_HOST = "192.168.56.103";
    static final Integer RABBITMQ_PORT = 5672 ;
    static final String RABBITMQ_USERNAME = "root";
    static final String RABBITMQ_PASSWORD = "root";
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建链接 工厂对象
        ConnectionFactory factory = new ConnectionFactory();
        // 设置 RabbitMQ 的主机 IP
        factory.setHost(RABBITMQ_HOST);
        // 设置 RabbitMQ 的端口号
        factory.setPort(RABBITMQ_PORT);
        // 设置访问的用户名
        factory.setUsername(RABBITMQ_USERNAME);
        // 设置访问密码
        factory.setPassword(RABBITMQ_PASSWORD);
        // 定义链接对象并实例化
        Connection connection = factory.newConnection();
        // 定义通道对象并实例化
        Channel channel = connection.createChannel();
        // 创建一个消息字符串
        String message = "Hello World~ RabbitMQ";
        
        // 通过通道声明一个队列 名称为 myQueue
        channel.queueDeclare("myQueue",true , false ,false ,null);
    
        // 通过通道发送消息到指定的队列当中
        channel.basicPublish("","myQueue",null,message.getBytes("UTF-8"));
    
        System.out.println("消息发送成功:" + message );
        channel.close();
        connection.close();
    }
}

queueDeclare(“myQueue”,true , false ,false ,null)

/**
* 管道声明一个队列
* 参数 1 : 队列名称取值任意
* 参数 2 : 是否是持久化的队列
* 参数 3 : 是否排外 如果是排外队列则这个队列只允许一个消费者监听
* 参数 4 : 是否自动删除队列  如果为 true 表示当前队列中没有消息 也没有消费者链接时就会自动删除这个队列
* 参数 5 : 队列中的属性设置 通常为 NULL
* */

📚 注意

  • 声明队列时,这个队列名称如果已经存在则放弃声明,如果队列不存在则会声明一个新的队列
  • 队列名称可以取值任意,但是要与消息接收时完全一致。
  • 这行代码可有可无,但是一定要在发送消息前,确认队列名已经存在在 RabbitMQ 中,否则就会出现问题。

basicPublish(“”,“myQueue”,null,message.getBytes(“UTF-8”))

/**
* 发送消息到指定队列当中
* 参数 1 : 交换机的名称 若参数为"" 则表示不适用交换机
* 参数 2 : 为 队列名称 或 RoutingKey 当指定了交换机名称后 其为 RoutingKey
* 参数 3 : 消息发送时的属性信息 通常为 NULL 即可
* 参数 4 : 为具体的消息数据的字节数组
* */

📚 运行结果如图所示:

在这里插入图片描述

步骤三:创建另一个项目工程 Module RabbitMQAndJava-rabbitmq-Receive-java

/***
 * @author: Alascanfu
 * @date : Created in 2022/5/28 1:04
 * @description: 消息接收类
 * @modified By: Alascanfu
 **/
public class Receive {
    static final String RABBITMQ_HOST = "192.168.56.103";
    static final Integer RABBITMQ_PORT = 5672 ;
    static final String RABBITMQ_USERNAME = "root";
    static final String RABBITMQ_PASSWORD = "root";
    
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBITMQ_HOST);
        factory.setPort(RABBITMQ_PORT);
        factory.setUsername(RABBITMQ_USERNAME);
        factory.setPassword(RABBITMQ_PASSWORD);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        
        channel.queueDeclare("myQueue",true , false ,false ,null);
        boolean autoAck = true ;
        String consumerTag = "";
        /**
         * 接收消息
         * 参数 1 : 队列名称
         * 参数 2 : 是否自动确认消息 true 表示自动确认 false 表示手动确认
         * 参数 3 : 为消息标签 用来区分不同的消费者 这里暂时为""
         * 参数 4 : 消费者回调方法 用于编写处理消息的具体代码(例如打印或者将消息写入数据库)
         * */
        channel.basicConsume("myQueue",autoAck,consumerTag,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String bodyStr = new String(body,"UTF-8");
                System.out.println(bodyStr);
            }
        });
        // 目的是为了持续监听
//        channel.close();
//        connection.close();
    }
}

📚 此时就可以尝试运行查看结果啦~ 切记必须先执行 发送消息 才能 接受消息。

📚 注意点:

  • Queue的消息只能被同一个消费者消费如果没有消费监听队列那么消息会存放到队列中持久化保存直到有消费者来消费这个消息,如果以有消费者监听队列则立即消费发送到队列中的消息

  • Queue的消息可以保证每个消息都一定能被消费

  • 使用了 basicConsume 方法之后, 会启动一个线程在持续的监听代码,如果队列中有信息的数据进入则会自动接收消息,因此不能关闭通道对象。

在这里插入图片描述

🔖 5-2 Java 绑定 Exchange 发送和接收消息

AMQP 协议中的核心思想就是生产者和消费者的解耦,生产者从不直接将消息发送给队列。

生产者通常不知道是否一个消息会被发送到队列当中,只是将消息发送到一个交换机当中。

  • 由 Exchange 来进行接收消息,然后 Exchange 通过按照特定的策略转发到 Queue 中进行存储。Exchange 就类似于交换机的作用,将各个消息分发到对应的队列中

实际应用中我们只需要定义好 Exchange 的路由策略,而生产者则不需要关心消息会发送到哪个 Queue 或被哪些 Consumer 消费。

在这种模式下生产者只面向 Exchange 发布消息,消费者只面向 Queue 消费消息,Exchange 定义了消息路由到 Queue 的规则,将各个层面的消息传递隔离开,使每一层只需要关心自己面向的下一层,降低了整体的耦合度。

⭐ Exchange的direct消息绑定

步骤一:编写 direct 消息发送类 com.alascanfu.rabbitmq.direct.Send

/***
 * @author: Alascanfu
 * @date : Created in 2022/5/28 2:43
 * @description: DirectExchange 消息发送类
 * @modified By: Alascanfu
 **/
public class Send {
    static String RABBITMQ_HOST = "192.168.56.103";
    static Integer RABBITMQ_PORT = 5672 ;
    static String RABBITMQ_USERNAME = "root" ;
    static String RABBITMQ_PASSWORD = "root" ;
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBITMQ_HOST);
        factory.setPort(RABBITMQ_PORT);
        factory.setUsername(RABBITMQ_USERNAME);
        factory.setPassword(RABBITMQ_PASSWORD);
        Connection connection = null;
        Channel channel = null;
        try{
            connection = factory.newConnection();
            channel = connection.createChannel();
            String msg = "HelloWorld~ Rabbitmq ~ Direct Exchange ~";
            String exchangeName = "myDirectExchange";
            String queueName = "myDirectQueue";
            String myRoutingKey = "myRoutingKey";
            // 很重要的 三行语句 如果需要采用 Direct 交换机 进行消息绑定分发 需要声明队列、声明交换机、明确绑定
            channel.queueDeclare(queueName,true ,false , false , null);
            channel.exchangeDeclare(exchangeName,"direct",true);
            /**
            * 参数一:声明队列的名称
            * 参数二:交换机名称
            * 参数三:队列与交换之间通过 routingKey 进行绑定
            */
            channel.queueBind(queueName , exchangeName , myRoutingKey);
            
            channel.basicPublish(exchangeName , "myRoutingKey",null,msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("消息发送成功~");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                if (channel != null){
                    channel.close();
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果如图所示

在这里插入图片描述

步骤二:编写 direct 消息接收类 com.alascanfu.rabbitmq.direct.Receive

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/2 17:52
 * @description: com.alascanfu.rabbitmq.direct.receive
 * @modified By: Alascanfu
 **/
public class Receive {
    static String RABBITMQ_HOST = "192.168.56.103";
    static Integer RABBITMQ_PORT = 5672;
    static String RABBITMQ_USER = "root";
    static String RABBITMQ_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBITMQ_HOST);
        factory.setPort(RABBITMQ_PORT);
        factory.setUsername(RABBITMQ_USER);
        factory.setPassword(RABBITMQ_PASS);
    
        Connection connection = null ;
        Channel channel = null ;
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String exchangeName = "myDirectExchange";
            Boolean autoAck = true ;
            String consumerTag = "";
            channel.queueDeclare("myDirectQueue",true,false ,false,null);
            
            channel.exchangeDeclare(exchangeName,"direct",true );
    		
            channel.queueBind("myDirectQueue" , exchangeName , "myRoutingKey");
    
            channel.basicConsume("myDirectQueue",autoAck,consumerTag,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body) throws IOException {
                    String bodyStr = new String(body, StandardCharsets.UTF_8);
                    System.out.println(bodyStr);
                }
            });
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        } finally {
//            if (connection != null && channel != null){
//                try {
//                    channel.close();
//                    connection.close();
//                } catch (IOException | TimeoutException e) {
//                    e.printStackTrace();
//                }
//            }
        }
    
    }
}

测试此时先打开消息接收端,当消息接收端打开之后,会默认开启一个后台线程,自动去监听对应的消息队列是否有消息。

因为这里设置了 autoAck 自动获取 所以只要与通道进行绑定的队列中还存在消息,那么就会一直获取

在这里插入图片描述

📚 注意点一:使用direct消息模式时必须要指定RoutingKey(路由键),将指定的消息绑定到指定的路由键上

channel.basicPublish(exchangeName , "myRoutingKey",null,msg.getBytes(StandardCharsets.UTF_8));

📚 注意点二:

1、使用 Exchange 的 direct 模式时接收者的 RoutingKey 必须要与发送时的 RoutingKey 完全一致否则无法获取消息

2、接收消息时队列名也必须要发送消息时的完全一致

channel.queueBind("myDirectQueue" , exchangeName , "myRoutingKey");
⭐ Exchange的Fanout消息绑定

步骤一:编写 Fanout 消息接收类 com.alascanfu.rabbitmq.fanout.Receive 、 com.alascanfu.rabbitmq.fanout.OtherReceive

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/2 20:05
 * @description: Fanout Exchange Receive
 * @modified By: Alascanfu
 **/
public class Receive {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672 ;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null;
        Channel channel = null ;
        
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String exchangeName = "myFanoutExchange";
            /**
             * 由于 Fanout 类型的交换机消息时类似于 广播的模式 , 它不需要绑定 RoutingKey
             * 而又可能有会有多个消息来接受这个交换机 中的数据 , 因此我们创建队列时要创建一个
             * 随机的队列名称。
             * 没有参数的 queueDeclare 方法会创建一个名字为随机的一个队列
             * 这个队列的数据是非持久化的
             * 是排外的(同时最多只允许有一个消费者监听当前队列)
             * 自动删除的 当没有任何消费者监听队列时就会自动删除该队列
             */
            String queueName = channel.queueDeclare().getQueue();
            channel.exchangeDeclare(exchangeName , BuiltinExchangeType.FANOUT , true );
            // 将这个随机的队列 绑定到交换机中, 由于是 fanout 类型的交换机 所以无需指定 routingKey
            channel.queueBind(queueName , exchangeName , "");
            
            Boolean autoAck = true ;
            channel.basicConsume(queueName , true ,"",new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body) throws IOException {
                    String bodyStr = new String(body, StandardCharsets.UTF_8);
                    System.out.println("Receive --- 1:" + bodyStr);
                }
            });
        }catch (Exception e){
            e.printStackTrace();
        }finally {
//            try {
//                if (channel != null){
//                    channel.close();
//                }
//                if (connection != null){
//                    connection.close();
//                }
//            } catch (IOException | TimeoutException e) {
//                e.printStackTrace();
//            }
        }
    }
}

当我们启动 Fanout Receive 时, RabbitMQ 中会创建两个随机队列名的随机队列

📚 注意点:

1、该队列是随机名称进行创建的

// 获取随机创建的队列的队列名
String queueName = channel.queueDeclare().getQueue();

2、查看源码 queueDeclare() 方法源码时总结

  • 这个队列名称是随机的 且 非可持久化的、排外的、自动删除的一个队列。
public com.rabbitmq.client.AMQP.Queue.DeclareOk queueDeclare() throws IOException {
    return this.queueDeclare("", false, true, true, (Map)null);
}
  • 源码中生成随机声明队列
com.rabbitmq.client.AMQP.Queue.DeclareOk ok = this.delegate.queueDeclare(queue, durable, exclusive, autoDelete, arguments);

步骤二:编写 Fanout 消息发送类 com.alascanfu.rabbitmq.fanout.Send

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/2 21:26
 * @description: fanout Exchange Send
 * @modified By: Alascanfu
 **/
public class Send {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672 ;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
    
        Connection connection = null ;
        Channel channel = null ;
        
        try {
            connection = factory.newConnection();
            channel = connection.createChannel() ;
            /**
             * 由于使用 Fanout 类型的交换机,因此消息的接收方肯定会有多个,因此
             * 不建议在消息发送时创建队列,以及绑定交换机,
             * 但是发送消息时至少需要确保知道交换机是存在的
             */
            String exchangeName = "myFanoutExchange";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT , true );
            String msg = "Hello World~ RabbitMQ ~ Fanout Exchange";
            
            channel.basicPublish(exchangeName, "" , null ,msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("Fanout Exchange : 消息发送成功~");
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            try {
                if (channel != null){
                    channel.close();
                }
                if (connection != null){
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
        
    }
}

结果演示以及注意事项:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

📚 注意点一:

  • 由于使用 Fanout 类型的交换机,因此消息的接收方肯定会有多个,因此不建议在消息发送时创建队列,以及绑定交换机,但是发送消息时至少需要确保知道交换机是存在的
  • 切记不要明确指定的队列名称并进行了与交换机的绑定,这么做没有任何的意义,因为,消费者的数量可能会很多,因此不能让所有消费者都全部监听同一个队列。
⭐ Exchange的Topic消息绑定

步骤一:编写 Topic 消息接收类 com.alascanfu.rabbitmq.topic.Receive_01 、 com.alascanfu.rabbitmq.topic.Receive_02、com.alascanfu.rabbitmq.topic.Receive_03

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/3 1:33
 * @description: Topic Exchange Receive
 * @modified By: Alascanfu
 **/
public class Receive_01 {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null;
        Channel channel = null;
        
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String exchangeName = "myTopicExchange";
            // 需要修改的两处地方 当前消费者从哪一个队列中获取消息 
            String queueName = "myTopicQueue_01";
            // 以及这个模式匹配的bindingKey 的设置 
            // 其中 * 代表匹配有且仅有一个单词
            // 其中 # 代表匹配 0 个 或多个单词
            String bindingKey = "CN";
            boolean autoAck = true ;
            channel.queueDeclare(queueName,true , false , false , null);
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,true );
            channel.queueBind(queueName , exchangeName , bindingKey );
            
            
            channel.basicConsume(queueName,autoAck ,"",new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body) throws IOException {
                    String bodyStr = new String(body , StandardCharsets.UTF_8);
                    System.out.println("Topic ---> RoutingKey=> CN :" + bodyStr);
                }
            });
            
            
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
//            try {
//                if (channel != null){
//                    channel.close();
//                }
//
//                if (connection != null){
//                    connection.close();
//                }
//            } catch (IOException | TimeoutException e) {
//                e.printStackTrace();
//            }
        }
    }
}

步骤二:编写 Topic 消息发送类 com.alascanfu.rabbitmq.topic.Send

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/3 1:33
 * @description: Topic Exchange Send
 * @modified By: Alascanfu
 **/
public class Send {
    static String RABBITMQ_HOST = "192.168.56.103";
    static Integer RABBITMQ_PORT = 5672;
    static String RABBITMQ_USERNAME = "root";
    static String RABBITMQ_PASSWORD = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBITMQ_HOST);
        factory.setPort(RABBITMQ_PORT);
        factory.setUsername(RABBITMQ_USERNAME);
        factory.setPassword(RABBITMQ_PASSWORD);
        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String exchangeName = "myTopicExchange";
            // 这里每次进行修改发送不同的消息 进行topic Exchange 按照匹配模式进行消息分发
            String bindingKey = "CN";
            String msg = "Hello World~ RabbitMQ ~ Hello Topic Exchange ~ bindingKey is " + bindingKey;
            channel.basicPublish(exchangeName,bindingKey,null,msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("消息已经成功发送到了 => Topic Exchange ~");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (channel != null) {
                    channel.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

测试

  • 从 Send 端发送不同的信息如:
    • CN
    • CN.IT
    • CN.IT.Alascanfu

Receive_03

在这里插入图片描述

Receive_02

在这里插入图片描述

Receive_01

在这里插入图片描述

⭐️ Topic Exchange 与 Fanout Exchange 关系与对比
  • Topic Exchange 与 Fanout Exchange 都是消息一对多的一种交换机类型,它和 fanout 都能实现一个消息同时发送给多个队列。

  • Fanout 更适合于使用在一个功能不同的进程来获取数据例如 手机 APP 的消息推送,一个 APP 可能会有很多的用户来进行安装,它们都会启动一个随机的队列来接收着自己的数据。

  • Topic 更适合于不同的功能模块之间接受同一个消息,例如商城 下单成功 发送到消息队列中去。例如 RoutingKey 为 order.success , 物流系统监听订单消息 order.logistics ,发票系统监听 order.invoice

  • Topic 可以使用随机的队列名也可以使用一个明确的队列名,但是如果应用在和订单等有关的功能当中,是需要有明确名称的队列名,并且要求为持久化的队列,保护用户订单数据。

🔖 5-3 Java 与 RabbitMQ 中事务的使用

什么是事务消息?

事务消息与数据库中的事务类似,只不过是MQ中的消息是要保证消息是否会全部发送成功,防止丢失消息的一种策略。

RabbitMQ 有两种方式来解决这个问题:

  • 通过 AMQP 提供的事务机制实现
  • 使用发送者确认模式实现
⭐ 事务的使用

事务的实现主要是对通道(Channel)进行设置,主要的方法有三个:

  • channel.txSelect() 声明启动事务模式
  • channel.txCommit() 提交事务
  • channel.txRollback() 回滚事务

步骤一:编写一个 com.alascanfu.rabbitmq.transaction.Send

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/4 1:08
 * @description: Transaction in RabbitMQ And Send Message By Direct Exchange
 * @modified By: Alascanfu
 **/
public class Send {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672 ;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null ;
        Channel channel = null ;
        
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String queueName = "transactionQueue";
            String exchangeName = "transactionDirectExchange";
            String bindingKey = "transactionMsg";
            String msg = "Transaction Message ~ HelloRabbitMQ";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT , true );
            channel.queueDeclare(queueName, true , false , false ,null);
            channel.queueBind( queueName, exchangeName, bindingKey);
            
            channel.basicPublish(exchangeName , bindingKey, null , msg.getBytes(StandardCharsets.UTF_8));
            // 用于完成第一个消息的发送时延 满足测试案例
            TimeUnit.SECONDS.sleep(2);
            System.out.println(10 / 0);
            channel.basicPublish(exchangeName , bindingKey, null , msg.getBytes(StandardCharsets.UTF_8));
            
            System.out.println("Transaction Message 发送到了 Direct Exchange 上, 消息发送成功~");
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            try {
                if (channel != null){
                    channel.close();
                }
        
                if (connection != null){
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

观察结果得出结论

当我们成功进行了第一条的消息发送,但是由于中间业务处理过程中导致了后续消息的丢失,此时消息队列中就只有第一个消息,而在错误发生之后的所有消息的都会丢失,破坏了消息的完整性。

步骤二:添加事务消息处理代码以保证消息的完整性加入队列

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/4 1:08
 * @description: Transaction in RabbitMQ And Send Message By Direct Exchange
 * @modified By: Alascanfu
 **/
public class Send {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672 ;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null ;
        Channel channel = null ;
        
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String queueName = "transactionQueue";
            String exchangeName = "transactionDirectExchange";
            String bindingKey = "transactionMsg";
            String msg = "Transaction Message ~ HelloRabbitMQ";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT , true );
            channel.queueDeclare(queueName, true , false , false ,null);
            channel.queueBind( queueName, exchangeName, bindingKey);
            // 开启消息事务处理
            channel.txSelect();
            channel.basicPublish(exchangeName , bindingKey, null , msg.getBytes(StandardCharsets.UTF_8));
            // 用于完成第一个消息的发送时延 满足测试案例
            TimeUnit.SECONDS.sleep(2);
            // 注释下面这行之后就可以保证消息的完整性一致提交,而不会导致事务失败
            //System.out.println(10 / 0);
            channel.basicPublish(exchangeName , bindingKey, null , msg.getBytes(StandardCharsets.UTF_8));
            // 当所有消息传送到队列完毕之后,才进行事务提交
            channel.txCommit();
            System.out.println("2 条 Transaction Message 发送到了 Direct Exchange 上, 消息发送成功~");
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            try {
                if (channel != null){
                    channel.close();
                }
        
                if (connection != null){
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

📚 注意点:

启动一个事务,启动事务之后所有写入到队列中的消息,必须要显式的调用 txCommit() 提交事务或者 txRollback()回滚事务。

提交事务,如果我们调用了txSelect()方法启动了事务,那么必须显式调用事务的提交,否则消息不会真正的写入到队列当中,提交时会将内存中的消息写入到队列并且释放数据。

暂时事务队列接收者是否产生影响?

我们再写一个消息接受类com.alascanfu.rabbitmq.transaction.Receive

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/4 1:48
 * @description: Transaction in RabbitMQ And Send Message By Direct Exchange Receive
 * @modified By: Alascanfu
 **/
public class Receive {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null;
        Channel channel = null;
        
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String queueName = "transactionQueue";
            String exchangeName = "transactionDirectExchange";
            String bindingKey = "transactionMsg";
            boolean autoAck = true ;
            // 开启消息事务处理
            channel.txSelect();
            channel.basicConsume(queueName,autoAck,"",new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties
                                               properties, byte[] body) throws IOException {
                    String bodyStr = new String(body,StandardCharsets.UTF_8);
                    System.out.println(bodyStr);
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
//            try {
//                if (channel != null) {
//                    channel.close();
//                }
//
//                if (connection != null) {
//                    connection.close();
//                }
//            } catch (IOException | TimeoutException e) {
//                e.printStackTrace();
//            }
        }
    }
}

📚 注意点:

  • 当消费者开启事务之后,即使不作为事务的提交,那么依然可以获取队列中的消息并且将消息从队列中移除掉。

为了解决上述出现的问题,以及解决事务处理导致系统效率过低的问题,就有了消息的发送确认模式~

🔖 5-4 消息的发送者确认模式

Confirm发送方确认模式使用和事务类似,也是通过设置Channel进行发送方确认的,最终达到确保所有的消息全部发送成功

⭐ 方式一:channel.waitForConfirms() 普通发送确认模式

步骤一:创建一个消息发送类用于测试 普通发送确认模式com.alascanfu.rabbitmq.transaction.waitforconfirms.Send

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/4 13:07
 * @description: com.alascanfu.rabbitmq.transaction.waitForConfirms.Send
 * @modified By: Alascanfu
 **/
public class Send {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672 ;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null ;
        Channel channel = null ;
        try {
            connection = factory.newConnection();
            channel = connection.createChannel() ;
            String exchangeName = "waitForConfirmsDirectExchange";
            String queueName = "waitForConfirmsQueue";
            String bindingKey = "waitForConfirmsBindingKey";
            boolean autoAck = true ;
            channel.exchangeDeclare(exchangeName , BuiltinExchangeType.DIRECT , true );
            channel.queueDeclare(queueName,true , false , false , null);
            channel.queueBind(queueName , exchangeName , bindingKey);
            
            channel.confirmSelect();
            long oldTime = System.currentTimeMillis();
            for (int i = 0; i < 1000; i++) {
                String msg = "Hello~ RabbitMQ ~ waitForConfirms " + i + "message";
                channel.basicPublish(exchangeName , bindingKey , null , msg.getBytes(StandardCharsets.UTF_8));
                boolean flag = channel.waitForConfirms();
            	System.out.println(i + "条消息是否完全发送成功 :" + flag);
            }
            System.out.println("共计用时:" + (System.currentTimeMillis() - oldTime )+ "ms");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (channel != null){
                    channel.close();
                }
        
                if (connection != null){
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

📚 值得注意的是:

  • 当我们开启发送者确认模式时——channel.confirmSelect()

  • 并且使用到了 channel.waitForConfirms() 时,会阻塞线程等待服务返回响应,用于判断是否发送成功,如果服务器确认消费已经发送成功返回true , 否则返回false。

  • 可以为上述方法 指定一个毫秒参数,用于确定我们需要等待服务确认的超时时间。

  • 如果超出了指定的时间以后则会抛出异常InterruptedException 表示服务器出现了问题,需要进行消息补发或者将消息缓存到 Redis 中稍后利用定时任务去补发。

  • 无论是返回 false 还是 抛出异常消息都有可能发送成功、有可能发送不成功。

  • 如果我们要求当前这个信息一定要发送到队列,例如订单数据,那我们可以采用消息补发。

什么是消息补发?

所谓的补发就是重新发送一次消息,可以采用递归或者利用 Redis + 定时任务来完成补发。

⭐ 方式二:channel.waitForConfirmsOrDie() 批量确认模式

步骤一:创建一个消息发送类用于测试 批量发送确认模式com.alascanfu.rabbitmq.transaction.waitforconfirmsOrDie.Send

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/4 15:21
 * @description: wait for confirms or die
 * @modified By: Alascanfu
 **/
public class Send {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String exchangeName = "waitForConfirmsOrDieDirectExchange";
            String queueName = "waitForConfirmsOrDieQueue";
            String bindingKey = "waitForConfirmsOrDieBindingKey";
            boolean autoAck = true;
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true);
            channel.queueDeclare(queueName, true, false, false, null);
            channel.queueBind(queueName, exchangeName, bindingKey);
            
            channel.confirmSelect();
            long oldTime = System.currentTimeMillis();
            for (int i = 0; i < 1000; i++) {
                String msg = "Hello~ RabbitMQ ~ waitForConfirmsOrDie " + i + "message";
                channel.basicPublish(exchangeName, bindingKey, null, msg.getBytes(StandardCharsets.UTF_8));
            }
            channel.waitForConfirmsOrDie();
            System.out.println("共计用时:" + (System.currentTimeMillis() - oldTime) + "ms");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (channel != null) {
                    channel.close();
                }
                
                if (connection != null) {
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

📚 注意点:

  • waitForConfirmsOrDie 批量消息确认,它会同时向服务中确认之前,当前通道中发送的所有的消息是否已经全部成功写入。

  • 该方法没有任何的返回值,如果服务器中有一条消息没有能够成功发送或者向服务器发送确认时服务器不可访问,都会被认为消息确认失败,可能有有消息没有发送成功,我们需要进行消息的补发

  • 如果无法向服务器获取确认信息,那么方法就会抛出 InterruptedException 异常,这时就需要补发消息到队列当中。

  • waitForConfirmsOrDie 方法也可以指定一个参数 timeout 用于等待服务器的确认时间,如果超过这个时间也会抛出异常,表示确认失败需要补发消息。

  • 批量消息确认的速度是比普通的消息确认要快,但是如果一旦出现了消息补发的情况,我们不能确定具体哪一条消息是没有完成发送,需要将本次发送的所有消息进行补发。

⭐ 方式三:channel.addConfirmListener() 异步监听发送方确认模式

步骤一:创建一个消息发送类用于测试异步监听发送方确认模式com.alascanfu.rabbitmq.transaction.confirmListener.Send

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/4 16:43
 * @description: confirmListener
 * @modified By: Alascanfu
 **/
public class Send {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String exchangeName = "waitForConfirmsListenerDirectExchange";
            String queueName = "waitForConfirmsListenerQueue";
            String bindingKey = "waitForConfirmsListenerBindingKey";
            boolean autoAck = true;
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true);
            channel.queueDeclare(queueName, true, false, false, null);
            channel.queueBind(queueName, exchangeName, bindingKey);
            
            channel.confirmSelect();
            long oldTime = System.currentTimeMillis();
            channel.addConfirmListener(new ConfirmListener() {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("已确认消息,标识:" + deliveryTag + "----多个消息:" + multiple);
                }
        
                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("未确认消息,标识:" + deliveryTag + " ---多个消息:" + multiple);
                }
            });
            TimeUnit.SECONDS.sleep(1);
            for (int i = 0; i < 1000; i++) {
                String msg = "Hello~ RabbitMQ ~ waitForConfirmsListener " + i + "message";
                channel.basicPublish(exchangeName, bindingKey, null, msg.getBytes(StandardCharsets.UTF_8));
                
            }
            TimeUnit.SECONDS.sleep(1);
            System.out.println("共计用时:" + (System.currentTimeMillis() - oldTime) + "ms");
        } catch (TimeoutException | IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if (channel != null) {
                    channel.close();
                }
                
                if (connection != null) {
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

handleAck(long l , boolean b)

消息确认之后的回调方法

📚 如果参数2 为 true 则表示本次确认同时确认了多条消息,消息等于当前参数1 (消息编号)的所有消息全部被确认 ,如果为 false 则表示只确认了当前编号的消息。

handleNAck(long l , boolean b)

消息没有确认的回调方法

如果这个方法被执行表示当前的消息没有被确认 需要进行消息补发

📚 如果参数2 为 true 则表示小于当前编号的所有消息可能都没有发送成功,需要进行消息补发,如果为false,则表示当前编号的消息没有发送成功,需要进行补发当前编号的消息。

参数

  • long l : 为被确认的消息的编号 从 1 开始自动递增用于标记当前是第几个消息
  • boolean b : 为当前消息是否同时的确认/ 没有确认 多个消息

🔖 5-5 消息的消费者确认模式

如何保证消息从队列可靠的到达消费者?

RabbitMQ 提供了消息确认机制(message acknowledgment)

消费者 在声明队列时,可以指定 noAck 参数,当 noAck = false 时,RabbitMQ 会等待消费者显式的返回 ack 信号后才从内存(和磁盘,如果是持久化消息的话)中去移去消息。

否则 ,RabbitMQ 会在队列中消息被消费后立即删除它。

在Consumer中Confirm模式中分为手动确认和自动确认。

basicAck()

  • 用于肯定确认,multiple参数用于多个消息确认。

basicRecover()

  • 是路由不成功的消息可以使用recovery重新发送到队列中。

basicReject()

  • 是接收端告诉服务器这个消息我拒绝接收,不处理,可以设置是否放回到队列中还是丢掉,而且只能一次拒绝一个消息,官网中有明确说明不能批量拒绝消息,为解决批量拒绝消息才有了basicNack。

basicNack()

  • 可以一次拒绝N条消息,客户端可以设置basicNack方法的multiple参数为true。

步骤一:创建一个消息发送类用于测试异步监听发送方确认模式com.alascanfu.rabbitmq.transaction.confirm.Receive

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/4 19:43
 * @description: 消息的消费确认模式 接受类
 * @modified By: Alascanfu
 **/
public class Receive {
    static String RABBIT_HOST = "192.168.56.103";
    static Integer RABBIT_PORT = 5672;
    static String RABBIT_USER = "root";
    static String RABBIT_PASS = "root";
    
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(RABBIT_HOST);
        factory.setPort(RABBIT_PORT);
        factory.setUsername(RABBIT_USER);
        factory.setPassword(RABBIT_PASS);
        
        Connection connection = null;
        Channel channel = null;
        
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String queueName = "transactionQueue";
            boolean autoAck = false;
            
            // 开启事务处理
            channel.txSelect();
            channel.basicConsume(queueName, autoAck, "", new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties
                                               properties, byte[] body) throws IOException {
                    long deliveryTag = envelope.getDeliveryTag();
                    Channel channel = super.getChannel();
                    if (!envelope.isRedeliver()){
                        String bodyStr = new String(body, StandardCharsets.UTF_8);
                        System.out.println(bodyStr);
                        channel.basicAck(deliveryTag,true);
                    }else {
                        // 程序 到了这里表示,这个消息之前已经被接收过了需要防重复处理
                        // 例如 查询数据库中是否已经添加了记录或者已经修改了记录
                        // 如果经过判断这条没有被处理完成则需要重新处理消息然后确认掉这条消息
                        // 如果已经处理过了 直接确认消息即可不需要进行其他处理操作了
                        channel.basicAck(deliveryTag, false);
                    }
                }
            });
            TimeUnit.SECONDS.sleep(2);
            // 开始事务提交
            channel.txCommit();
            System.out.println("消息接收成功~");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
//            try {
//                if (channel != null) {
//                    channel.close();
//                }
//
//                if (connection != null) {
//                    connection.close();
//                }
//            } catch (IOException | TimeoutException e) {
//                e.printStackTrace();
//            }
        }
    }
}

📚 注意点:

  • 如果我们只是接收的消息但是还没有来得及处理前,应用就崩溃或者在进行处理的时候,例如像数据库中写入数据,但是数据库此时不可用,那么由于消息是 autoAck 的,那么这个消息会在接收完成之后自动从队列中删除,此时便会丢失数据。

envelope.isRedeliver

  • 获取当前消息是否被接收过一次,如果返回false,表示消息之前没有被接受过。如果返回值为true 则表示之前这个消息被接受过,可能处理完成,因此我们要进行消息的防重复处理。

📑 SpringBoot 与 RabbitMQ 整合

🔖 模块化下的 快速整合 RabbitMQ

步骤一:创建一个新的 SpringBoot 工程 导入相关所需使用的依赖

springboot-rabbitmq-mail

pom.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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>common</module>
        <module>producer</module>
        <module>consumer</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.alascanfu</groupId>
    <artifactId>springboot-rabbitmq-mail</artifactId>
    <version>0.0.1</version>
    <name>springboot-rabbitmq-mail</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <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>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

步骤二:创建一个通用子模块 common

com.alascanfu.config.RabbitMQConfig

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 1:11
 * @description: RabbitMQConfig
 * @modified By: Alascanfu
 **/
public class RabbitMQConfig {
    /**
    * RabbitMQ 的队列主题名称
    */
    public static final String RABBITMQ_DEMO_TOPIC = "rabbitmq.demo.topic";
    
    /**
    * RabbitMQ 的DIRECT 交换机名称
    */
    public static final String RABBIT_DEMO_DIRECT_EXCHANGE = "rabbitmq.demo.direct.exchange";
    
    /**
    * RabbitMQ 的 DIRECT 交换机和队列绑定的 匹配键 DirectRouting
    */
    public static final String RABBIT_DEMO_DIRECT_ROUTING = "rabbitmq.demo.direct.routing";
    
    /**
     * RabbitMQ的FANOUT_EXCHANG交换机类型的队列 A 的名称
     */
    public static final String FANOUT_EXCHANGE_QUEUE_TOPIC_A = "fanout.A";
    
    /**
     * RabbitMQ的FANOUT_EXCHANG交换机类型的队列 B 的名称
     */
    public static final String FANOUT_EXCHANGE_QUEUE_TOPIC_B = "fanout.B";
    
    /**
     * RabbitMQ的FANOUT_EXCHANG交换机类型的名称
     */
    public static final String FANOUT_EXCHANGE_DEMO_NAME = "fanout.exchange.demo.name";
    
    /**
     * RabbitMQ的TOPIC_EXCHANGE交换机名称
     */
    public static final String TOPIC_EXCHANGE_DEMO_NAME = "topic.exchange.demo.name";
    
    /**
     * RabbitMQ的TOPIC_EXCHANGE交换机的队列A的名称
     */
    public static final String TOPIC_EXCHANGE_QUEUE_A = "topic.queue.a";
    
    /**
     * RabbitMQ的TOPIC_EXCHANGE交换机的队列B的名称
     */
    public static final String TOPIC_EXCHANGE_QUEUE_B = "topic.queue.b";
    
    /**
     * RabbitMQ的TOPIC_EXCHANGE交换机的队列C的名称
     */
    public static final String TOPIC_EXCHANGE_QUEUE_C = "topic.queue.c";
    
    /**
     * HEADERS_EXCHANGE交换机名称
     */
    public static final String HEADERS_EXCHANGE_DEMO_NAME = "headers.exchange.demo.name";
    
    /**
     * RabbitMQ的HEADERS_EXCHANGE交换机的队列A的名称
     */
    public static final String HEADERS_EXCHANGE_QUEUE_A = "headers.queue.a";
    
    /**
     * RabbitMQ的HEADERS_EXCHANGE交换机的队列B的名称
     */
    public static final String HEADERS_EXCHANGE_QUEUE_B = "headers.queue.b";
}

步骤三:创建一个生产者模块用于模拟消息的产生

步骤3-1 配置 application.yaml 文件

application.yaml

spring:
  rabbitmq:
    host: 192.168.56.103
    port: 5672
    username: root
    password: root
    virtual-host: /
server:
  port: 8081

步骤3-2 书写 生产者的SpringBootApplication 主启动类

RabbitMQDemoProducerApplication.java

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 2:14
 * @description: RabbitMQDemoProducerApplication
 * @modified By: Alascanfu
 **/
@SpringBootApplication
public class RabbitMQDemoProducerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RabbitMQDemoProducerApplication.class,args);
    }
}

步骤3-3 书写 一个简单的通过 Direct Exchange 转发消息的交换机配置类

com.alascanfu.config.rabbitmq.direct.DirectRabbitConfig.java

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 1:26
 * @description: com.alascanfu.config.rabbitmq.direct.DirectRabbitConfig
 * @modified By: Alascanfu
 **/
@Configuration
@Component
public class DirectRabbitConfig implements BeanPostProcessor {
    
    @Resource
    private RabbitAdmin rabbitAdmin;
    
    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        // 只有设置为 true,spring 才会加载 RabbitAdmin 这个类
        rabbitAdmin.setAutoStartup(true);
        return rabbitAdmin;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        //创建交换机
        rabbitAdmin.declareExchange(rabbitmqDemoDirectExchange());
        //创建队列
        rabbitAdmin.declareQueue(rabbitmqDemoDirectQueue());
        return null;
    }
    
    /**
     *功能描述
     * name : 队列名称
     * durable : 是否持久化
     * exclusive : 是否独享 , 排外的 如果设置为 true , 定义为排他队列 , 则只有创建者可以使用此队列。也就是private私有的。
     * autoDelete: 是否自动删除。也就是临时队列。当最后一个消费者断开连接后,会自动删除。
     * @date 2022/6/5
     *  @author Alascanfu
     */
    @Bean
    public Queue rabbitmqDemoDirectQueue(){
        return new Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC,true ,false ,false );
    }
    
    
    @Bean
    public DirectExchange rabbitmqDemoDirectExchange(){
        // Direct 交换机
        return new DirectExchange(RabbitMQConfig.RABBIT_DEMO_DIRECT_EXCHANGE , true , false );
    }
    
    @Bean
    public Binding bindDirect(){
        //链式写法,绑定交换机和队列,并设置匹配键
        return BindingBuilder
            //绑定队列
            .bind(rabbitmqDemoDirectQueue())
            //到交换机
            .to(rabbitmqDemoDirectExchange())
            //并设置匹配键
            .with(RabbitMQConfig.RABBIT_DEMO_DIRECT_ROUTING);
    }
}

步骤3-4 书写调用业务的 service 层 以及其实现类 、Controller 业务逻辑

com.alascanfu.service.RabbitMQService

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 1:37
 * @description: 生产者发送消息的 Service 类
 * @modified By: Alascanfu
 **/
public interface RabbitMQService {
    /**
     * 功能描述
     * RabbitMQService 发送消息
     * @date 2022/6/5
     * @author Alascanfu
     */
    public String sendMsg(String msg) ;
}

com.alascanfu.service.impl.RabbitMQServiceImpl

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 1:40
 * @description: RabbitMQServiceImpl RabbitMQ消息服务的实现类
 * @modified By: Alascanfu
 **/
@Service
public class RabbitMQServiceImpl implements RabbitMQService {
    // 日期格式化
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    @Resource
    private RabbitTemplate rabbitTemplate ;
    
    @Override
    public String sendMsg(String msg) {
        try {
            String msgId = UUID.randomUUID().toString().replace("-","").substring(0,32);
            String sendTime = sdf.format(new Date());
            Map<String , Object> map = new HashMap<>();
            map.put("msgId" , msgId);
            map.put("sendTime",sendTime);
            map.put("msg",msg);
            rabbitTemplate.convertAndSend(
                RabbitMQConfig.RABBIT_DEMO_DIRECT_EXCHANGE ,
                RabbitMQConfig.RABBIT_DEMO_DIRECT_ROUTING ,
                map);
            return "ok";
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }
}

com.alascanfu.controller.RabbitMQController

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 1:51
 * @description: RabbitMQ Controller
 * @modified By: Alascanfu
 **/
@RestController
@RequestMapping("/mail/rabbitmq")
public class RabbitMQController {
    @Autowired
    private RabbitMQService rabbitMQService;
    
    /**
     *功能描述
     * 发送消息
     * @date 2022/6/5
     *  @author Alascanfu
     */
    
    @PostMapping("/sendMsg")
    public String sendMsg(@RequestParam(name = "msg")String msg) throws IOException{
        return rabbitMQService.sendMsg(msg);
    }
}

步骤四:创建一个 consumer 消费者模块用于测试生产的消息被消费

步骤4-1 配置yaml

spring:
  rabbitmq:
    host: 192.168.56.103
    port: 5672
    username: root
    password: root

步骤4-2 编写consumer该模块的SpringBoot应用主启动类

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 2:02
 * @description: Start RabbitMQConsumerAppilcation
 * @modified By: Alascanfu
 **/
@SpringBootApplication
public class RabbitMQConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RabbitMQConsumerApplication.class ,args);
    }
}

步骤4-3 编写 com.alascanfu.demo.RabbitMQDemoConsumer

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 2:00
 * @description: com.alascanfu.demo.RabbitMQDemoConsumer
 * @modified By: Alascanfu
 **/
@Component
@RabbitListener(queuesToDeclare = @Queue(name = RabbitMQConfig.RABBITMQ_DEMO_TOPIC))
public class RabbitMQDemoConsumer {
    enum Action{
        // 处理成功
        SUCCESS ,
        // 可以重试的错误 , 消息重回队列
        RETRY ,
        // 无需重试的错误,拒绝消息,并从队列中删除
        REJECT
    }
    
    @RabbitHandler
    public void process(Map map){
        System.out.println("消费者 RabbitMQDemoConsumer 从 RabbitMQ 服务端消费消息" + map.toString());
    }
}

步骤五:因为上面已经配置好了,无论是先开启生产者还是消费者,默认都会检查队列是否存在,如果不存在则会自动创建。 所以可以放心测试,这里展示一下演示成果。

  • 启动 RabbitMQConsumerApplication.java 与 RabbitMQDemoProducerApplication.java

步骤5-1 通过 postman 模拟post请求向 producer 发送请求生产消息

http://localhost:8081/mail/rabbitmq/sendMsg?msg=Hello~SpringBoot And RabbitMQ~~

在这里插入图片描述

步骤5-2 观察 消费者控制台的变化

在这里插入图片描述

📚 大功告成~

🔖 Fanout Exchange

步骤一:配置 Fanout Exchange 类型的交换机 以及 A、B 两个队列,并且进行交换机与队列绑定,Fanout Exchange 不需要 RoutingKey 进行绑定

com.alascanfu.config.rabbitmq.fanout.FanoutRabbitConfig

/**
 * @author: Alascanfu
 * @date : Created in 2022/6/5 17:08
 * @description: Fanout Exchange Config
 * @modified By: Alascanfu
 */
@Configuration
@Component
public class FanoutRabbitConfig implements BeanPostProcessor {
    
    @Autowired
    private RabbitAdmin rabbitAdmin;
    @Autowired
    DirectRabbitConfig directRabbitConfig;

    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory){
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        rabbitAdmin.setAutoStartup(true);
        return rabbitAdmin;
    }
    
    @Bean
    public Queue fanoutExchangeQueueA(){
        return new Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_A ,true ,false , false , null);
    }
    
    @Bean
    public Queue fanoutExchangeQueueB(){
        return new Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_B ,true ,false , false , null);
    }
    
    @Bean
    public FanoutExchange rabbitmqDemoFanoutExchange(){
        return new FanoutExchange(RabbitMQConfig.FANOUT_EXCHANGE_DEMO_NAME , true , false );
    }
    
    @Bean
    public Binding bindQueueA(){
        return BindingBuilder
            .bind(fanoutExchangeQueueA())
            .to(rabbitmqDemoFanoutExchange());
    }
    
    @Bean
    public Binding bindQueueB(){
        return BindingBuilder
            .bind(fanoutExchangeQueueB())
            .to(rabbitmqDemoFanoutExchange());
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        rabbitAdmin.declareExchange(rabbitmqDemoFanoutExchange());
        rabbitAdmin.declareQueue(fanoutExchangeQueueA());
        rabbitAdmin.declareQueue(fanoutExchangeQueueB());
        rabbitAdmin.declareExchange(directRabbitConfig.rabbitmqDemoDirectExchange());
        rabbitAdmin.declareQueue(directRabbitConfig.rabbitmqDemoDirectQueue());
        return null;
    }
}

步骤二: 修改一下之前写好的RabbitMQService 接口 与 RabbitMQServiceImpl .java

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 1:37
 * @description: 生产者发送消息的 Service 类
 * @modified By: Alascanfu
 **/
public interface RabbitMQService {
    /**
     * 功能描述
     * RabbitMQService 发送消息
     * @date 2022/6/5
     * @author Alascanfu
     */
    public String sendMsg(String msg,String mode) ;
}

RabbitMQServiceImpl

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 1:40
 * @description: RabbitMQServiceImpl RabbitMQ消息服务的实现类
 * @modified By: Alascanfu
 **/
@Service
public class RabbitMQServiceImpl implements RabbitMQService {
    // 日期格式化
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    @Resource
    private RabbitTemplate rabbitTemplate ;
    
    @Override
    public String sendMsg(String msg , String mode) {
        Map<String, Object> map = getMessage(msg);
        try {
            if (mode.equals("direct")){
                rabbitTemplate.convertAndSend(
                    RabbitMQConfig.RABBIT_DEMO_DIRECT_EXCHANGE ,
                    RabbitMQConfig.RABBIT_DEMO_DIRECT_ROUTING ,
                    map);
            }else if (mode.equals("fanout")){
                rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EXCHANGE_DEMO_NAME
                ,"",map);
            }
            return "ok";
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }
    
    public Map<String , Object> getMessage(String msg){
        String msgId = UUID.randomUUID().toString().replace("-","").substring(0,32);
        String sendTime = sdf.format(new Date());
        Map<String , Object> map = new HashMap<>();
        map.put("msgId" , msgId);
        map.put("sendTime",sendTime);
        map.put("msg",msg);
        return map;
    }
}

步骤三:编写consumer 用于监听 RabbitMQ 消息

FanoutExchangeConsumerA

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 17:30
 * @description: FanoutExchangeConsumerA
 * @modified By: Alascanfu
 **/
@Component
@RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_A))
public class FanoutExchangeConsumerA {
    
    @RabbitHandler
    public void process(Map<String, Object> map) {
        System.out.println("队列A收到消息:" + map.toString());
    }
}

FanoutExchangeConsumerB

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 17:35
 * @description: FanoutExchangeConsumerB
 * @modified By: Alascanfu
 **/
@Component
@RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_B))
public class FanoutExchangeConsumerB {
    @RabbitHandler
    public void process(Map<String, Object> map) {
        System.out.println("队列B收到消息:" + map.toString());
    }
}

步骤四:利用postman模拟请求 生产消息 观察消费者的控制台查看输入输出

http://localhost:8081/mail/rabbitmq/sendMsg?msg=Hello~SpringBoot And RabbitMQ Fanout~~&mode=fanout

在这里插入图片描述
在这里插入图片描述

🔖 Topic Exchange

步骤一:配置 Topic Exchange 类型的交换机 以及 A、B 、 C 三个队列,并且进行交换机与队列绑定,Topic Exchange 需要 RoutingKey 进行绑定 ,routingKey 中可以包含 通配符,故topic Exchange 也被称之为 通配符交换机

com.alascanfu.config.rabbitmq.topic.TopicRabbitConfig

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 20:37
 * @description: Topic Exchange RabbitMQ Config
 * @modified By: Alascanfu
 **/
@Configuration
@Component
public class TopicRabbitConfig {
    
    @Bean
    public TopicExchange rabbitmqDemoTopicExchange(){
        return new TopicExchange(RabbitMQConfig.TOPIC_EXCHANGE_DEMO_NAME , true , false);
    }
    
    @Bean
    public Queue rabbitmqTopicQueueA(){
        return new Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_A);
    }
    
    @Bean
    public Queue rabbitmqTopicQueueB(){
        return new Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_B);
    }
    
    @Bean
    public Queue rabbitmqTopicQueueC(){
        return new Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_C);
    }
    
    @Bean
    public Binding bindingTopicA(){
        return BindingBuilder.bind(rabbitmqTopicQueueA())
            .to(rabbitmqDemoTopicExchange())
            .with("rabbit.*");
    }
    
    @Bean
    public Binding bindingTopicB(){
        return BindingBuilder.bind(rabbitmqTopicQueueB())
            .to(rabbitmqDemoTopicExchange())
            .with("a.*");
    }
    
    @Bean
    public Binding bindingTopicC(){
        return BindingBuilder.bind(rabbitmqTopicQueueC())
            .to(rabbitmqDemoTopicExchange())
            .with("a.#");
    }
}

步骤二:改写 RabbitMQServiceImpl 类,因为此时接口需要获取包含 routingKey 信息的请求响应

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 1:40
 * @description: RabbitMQServiceImpl RabbitMQ消息服务的实现类
 * @modified By: Alascanfu
 **/
@Service
public class RabbitMQServiceImpl implements RabbitMQService {
    // 日期格式化
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    @Resource
    private RabbitTemplate rabbitTemplate ;
    
    @Override
    public String sendMsg(String msg , String mode , String routingKey) {
        Map<String, Object> map = getMessage(msg);
        try {
            if (mode.equals("direct")){
                rabbitTemplate.convertAndSend(
                    RabbitMQConfig.RABBIT_DEMO_DIRECT_EXCHANGE ,
                    RabbitMQConfig.RABBIT_DEMO_DIRECT_ROUTING ,
                    map);
            }else if (mode.equals("fanout")){
                rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EXCHANGE_DEMO_NAME
                ,"",map);
            }else if (mode.equals("topic")){
                rabbitTemplate.convertAndSend(RabbitMQConfig.TOPIC_EXCHANGE_DEMO_NAME
                ,routingKey,map);
            }
            return "ok";
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }
    
    public Map<String , Object> getMessage(String msg){
        String msgId = UUID.randomUUID().toString().replace("-","").substring(0,32);
        String sendTime = sdf.format(new Date());
        Map<String , Object> map = new HashMap<>();
        map.put("msgId" , msgId);
        map.put("sendTime",sendTime);
        map.put("msg",msg);
        return map;
    }
}

📚 对实现类的改写也需要改写接口以及 Controller 层。

步骤三:编写 TopicExchangeConsumerA 、TopicExchangeConsumerB 、TopicExchangeConsumerC 三个针对于匹配不同routingKey获取消息的消费者

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 20:56
 * @description: TopicExchangeConsumerA
 * @modified By: Alascanfu
 **/
@Component
@RabbitListener(queuesToDeclare = @Queue(name = RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_A))
public class TopicExchangeConsumerA {
    @RabbitHandler
    public void process(Map<String , Object> map){
        System.out.println("队列[" + RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_A+ "]收到消息:" + map.toString());
    }
}


/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 20:58
 * @description: TopicExchangeConsumerB
 * @modified By: Alascanfu
 **/
@Component
@RabbitListener(queuesToDeclare = @Queue(name = RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_B))
public class TopicExchangeConsumerB {
    
    @RabbitHandler
    public void process(Map<String , Object> map){
        System.out.println("队列[" + RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_B+ "]收到消息:" + map.toString());
    }
}

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/5 20:59
 * @description: TopicExchangeConsumerC
 * @modified By: Alascanfu
 **/
@Component
@RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_C))
public class TopicExchangeConsumerC {
    @RabbitHandler
    public void process(Map<String , Object> map){
        System.out.println("队列[" + RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_C + "]收到消息:" + map.toString());
    }
}

步骤四:开启消息生产者与消费者,通过postman 模拟消息请求传入三个不同的routingKey,在消费者的终端查看运行结果

http://localhost:8081/mail/rabbitmq/sendMsg?msg=Hello~SpringBoot And RabbitMQ Topic~~&mode=topic&routingKey=rabbit.mail
http://localhost:8081/mail/rabbitmq/sendMsg?msg=Hello~SpringBoot And RabbitMQ Topic~~&mode=topic&routingKey=a.b
http://localhost:8081/mail/rabbitmq/sendMsg?msg=Hello~SpringBoot And RabbitMQ Topic~~&mode=topic&routingKey=a

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alascanfu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值