RabbitMQ入门

思维导图

1.RabbitMQ的概念

1.1 什么是MQ?

MQ全称为Message Queue(消息队列),是在消息传输过程中保存消息的容器,多用于系统之间的异步通信

1.1.1 同步通信

同步通信:甲乙两人面对面交流,你一句我一句必须同步进行,两人除此之外不做任何事情,就像打电话

同步通信

1.1.2 异步通信

异步通信:异步通信相当于通过第三方转述对话,可能有消息的延迟,但不需要二人时刻保持联系,消息传给第三方后,两人可以做其他自己想做的事情,当需要获取对方说话的内容时,直接从消息队列里获取即可。就像发微信

异步通信

1.1.2 理解消息队列

消息 就是两台计算机间传送的数据单位;本质上就是一段数据,它能被一个或者多个应用程序所理解,是应用程序之间传递的信息载体。消息可以非常简单,例如只包含文本字符串;也可以更复杂地嵌入对象。

队列 是数据结构中概念。在队列中,数据先进先出,后进后出,犹如排队做核酸。

消息队列 MQ是把消息和队列结合起来,称之为消息队列(Message Queue)。把要传输的数据(消息)与队列进行绑定,用队列先进先出机制来实现消息传递。消息队列由 生产者 和 消费者 两部分构成;生产者主要负责产生消息并把消息放入队列中,再由消费者去处理。消费者可以到指定队列中获取消息,或者订阅相应的队列,最后由MQ服务端进行消息推送。

订阅 订阅就是为消费者服务的,消费者提前订阅,当消息队列中有消息产出时,自动去获取消息进行消费。生活中有很多这种例子,比如购买腾讯、优酷等视频会员时就会有订阅模式,当你的会员到期时,会自动帮你完成续费。
队列

1.1.3MQ的优势和劣势

1、优势和作用

应用解耦 例如在我们非常熟悉电商系统平台中,用户下订单需要调用订单系统,订单系统需要调用库存系统、支付系统、物流系统等进行数据交互来完成整个下单业务,这种把整个业务都耦合在一起。很容易出现我们经常遇见的问题:

①、当中间某个环节库存系统或支付系统出现异常或故障,会造成整个订单系统崩溃无法完成订单业务;

②、当我们需求开发时,可能会新增一个其他系统的业务与当前的订单系统关联并进行数据交互,此时就必须修改订单系统的代码。如果系统壮大,业务越来越多,越来越复杂时,随之带来的维护成本也会成指数增加。
应用解耦
由此我们不难看出 系统耦合性越高,容错性和可维护性就会越低。如果在系统中引入MQ,即订单系统将消息先发送到MQ中,MQ再将数据消息转发到其他系统,问题就可以得到解决,MQ天生就是来干这个事的。那么当我们加入MQ以后整个系统会发生什么变化?

①、由于订单系统只发消息给MQ,不直接对接其他系统,如果其他任何一个系统出现异常或故障时,不影响整个订单业务的执行。当异常或故障解决以后,程序自动从MQ获取数据信息完成后续相关业务逻辑即可。

②、如果需求修改,新增了一个业务系统,此时无需修改其他系统的代码,只需修改MQ将消息发送给新增的系统即可,实现数据信息的可靠有效传递。使用MQ让系统应用间进行解耦,提升整个系统的容错性和可维护性
使用MQ解耦
异步提速 开发一款App系统,其登录注册是最为常见的需求,例如注册的业务有手机注册入库、绑定邮箱验证、手机短信验证。

异步提速
用户的注册操作需要等待350ms才能得到响应,如果邮件验证系统/手机短信通知系统耗时比较长,那么用户的响应随之就会变长,非常影响客户的体验度,而当使用了MQ后,客户的体验度飙升。

异步提速
削峰填谷 假设系统承载的QPS是1000,如果流量顶峰时达到50000,则会造成系统压垮崩溃。使用了MQ之后,限制消费消息的速度为1000,把数据产生的压力放在MQ中,系统的高峰就会被削掉,这个过程叫 削峰,因为消息积压的数据比较多,在高峰期过后的一段时间内,消费消息的速度还会继续维持在这个速度,直到把积压的消息消费完,这个过程叫 填谷。这样可以大大提高系统的稳定性和用户体验。

2、劣势

系统可用性降低: 系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。
系统复杂度提高: MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
一致性问题 : A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败,则会造成数据处理的不一致。

1.1.4 MQ的应用场景

1、高峰流量:抢红包、秒杀活动、抢火车票等这些业务场景都是短时间内需要处理大量请求,如果直接连接系统处理业务,会耗费大量资源,有可能造成系统瘫痪。 而使用MQ后,可以先让用户将请求发送到MQ中,MQ会先保存请求消息,不会占用系统资源,且MQ会进行消息排序,先请求的秒杀成功,后请求的秒杀失败。
高峰流量
2、消息分发:如电商网站要推送促销信息,该业务耗费时间较多,但对时效性要求不高,可以使用MQ做消息分发。

消息分发
3、数据同步:假如我们需要将数据保存到数据库之外,还需要一段时间将数据同步到缓存(如Redis)、搜索引擎(如Elasticsearch)中。此时可以将数据库的数据作为消息发送到MQ中,并同步到缓存、 搜索引擎中。

数据同步
4、异步处理:在电商系统中,订单完成后,需要及时的通知子系统(进销存系统发货,用户服务积分,发送短信)进行下一步操作。为了保证订单系统的高性能,应该直接返回订单结果,之后让MQ通知子系统做其他非实时的业务操作。这样能保证核心业务的高效及时

异步处理
5、离线处理:在银行系统中,如果要查询近十年的历史账单,这是非常耗时的操作。如果发送同步请求,则会花费大量时间等待响应。此时使用MQ发送异步请求,等到查询出结果后获取结果即可。

离线处理

1.1.5 AMQP

1、什么是 AMQP : 即Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,专门为消息中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受不同中间件产品,不同开发语言等条件的限制。2006年AMQP规范发布,类比HTTP。

2、AMQP工作过程: 生产者(Publisher)将消息发布到交换机(Exchange),交换机根据规则将消息分发给交换机绑定的队列(Queue),队列再将消息投递给订阅了此队列的消费者
AMQP

1.1.6工作原理

工作原理

  • Producer【消息的生产者】 一个向交换机发布消息的客户端应用程序。
  • Connection【连接】 生产者/消费者和RabbitMQ服务器之间建立的TCP连接。
  • Channel【信道】 是TCP里面的虚拟连接。例如:Connection相当于电缆,Channel相当于独立光纤束,一条TCP连接中可以创建多条信道,增加连接效率。无论是发布消息、接收消息、订阅队列都是通过信道完成的。
  • Broker 消息队列服务器实体。即RabbitMQ服务器
  • Virtual Host【虚拟主机】 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换机、绑定和权限机制。当多个不同的用户使用同一个RabbitMQ服务器时,可以划分出多个虚拟主机。RabbitMQ默认的虚拟主机路径是 /
  • Exchange【交换机】 用来接收生产者发送的消息,并根据分发规则,将这些消息分发给服务器中的队列中。不同的交换机有不同的分发规则。
  • Queue【消息队列】 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。消息一直在队列里面,等待消费者链接到这个 队列将其取走。
  • Binding【绑定】 消息队列和交换机之间的虚拟连接,绑定中包含路由规则,绑定信息保存到交换机的路由表中,作为消息的分发依据。
  • Consumer【消息的消费者】 表示一个从消息队列中取得消息的客户端应用程序。

2.RabbitMQ的安装

RabbitMQ是使用Erlang语言编写的,所以在安装RabbitMQ前需要先安装Erlang环境。本次从Windows11、Linux、Docker三个维度分别介绍 RabbitMQ的安装。

RabbitMQ最新版 (本安装下载版本统一为 Erlang 24.2RabbitMQ 3.9.11)针对所有系统版本的下载地址在 这里 GitHub

2.1安装Erlang

2.1.1 Windows安装

1、官网下载对应系统版本的 Erlang 安装包
erlang
2、安装 Erlang
erlang
3、配置环境变量
erlang
4、打开 cmd 输入 erl -version 出现版本号即安装成功
erlang

2.1.2 Linux 安装

1、安装Erlang所需的依赖

yum install -y epel-release

2、添加存储库条目

wget https://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm

rpm -Uvh erlang-solutions-1.0-1.noarch.rpm

3、安装erlang

yum install -y erlang

4、验证是否安装成功

erl -version

2.2 安装RabbitMQ

2.2.1 Windows安装

1.选择后缀为 .exe 的 RabbitMQ 文件下载安装
rabbitmq
2.CMD 进入终端 进入到安装目录 执行 rabbitmq-plugins enable rabbitmq_management 命令安装管理页面的插件,

安装
如果出现以上图片提示,重启下系统 或者输入以下命令

# set ERLANG_HOME=Erlang的安装目录
set ERLANG_HOME=C:\Program Files\Erlang OTP

启动rabbitMQ服务:rabbitmq-service.bat start 双击 rabbitmq-service.bat 即完成安装。

2.2.2 Linux安装

1、为了外部能够正常访问RabbitMQ服务,先关闭防火墙

# 关闭运行的防火墙
systemctl stop firewalld.service

# 禁止防火墙自启动
systemctl disable firewalld.service

2、RabbitMQ是通过主机名进行访问的,必须给服务器添加主机名

# 修改文件
vim /etc/sysconfig/network

# 添加如下内容,HOSTNAME 可以根据自己需要自定义
NETWORKING=yes

HOSTNAME=myRabbitmq

# 修改文件
vim /etc/hosts

# 添加如下内容
服务器ip myRabbitmq

3、在根目录或自定义目录下载 RabbitMQ

# 这里自建 download 文件 用于存放下载的文件
mkdir download

# 下载文件
wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.11.3/rabbitmq-server-3.11.3.tar.xz

# 解压文件
tar -zxvf rabbitmq-server-3.11.3.tar.xz

# 重命名
mv rabbitmq-server-3.11.3.tar.xz rabbitmq

# 移动文件,一般服务器的软件安装 放在 /usr/local/ 目录里
mv rabbitmq /usr/local/

4、配置环境变量

# 编辑/etc/profile文件
vim /etc/profile

#添加如下内容
export PATH=$PATH:/usr/local/rabbitmq/sbin

# 运行文件,让修改内容生效
source /etc/profile

5、配置和启动

# 进入到 rabbitmq 安装目录
cd /usr/local/rabbitmq

# 开启管控台插件 RabbitMQ 管控界面
rabbitmq-plugins enable rabbitmq_management

#启动rabbitmq
rabbitmq-server -detached

#停止rabbitmq
rabbitmqctl stop

6、通过Web页面 访问RabbitMQ。路径地址如下 路径: http://ip地址:15672 ,用户名和密码默认都是 guest
7、默认情况下 guest 账户只允许本地使用,如果有需要解除这种限制可以把配置修改成允许使用 guest远程访问**【注意:通常情况下这里不做任何修改配置,因为一旦允许guest 远程访问,将会把RabitMQ管控平台暴露出去,他人就可以用 guest 用户和密码进行登录操作,数据非常非常不安全。所以,一般情况是重新配置账号和密码。】**
管控台

# 创建配置文件夹
mkdir -p /usr/local/rabbitmq/etc/rabbitmq

# 创建配置文件
vim /usr/local/rabbitmq/etc/rabbitmq/rabbitmq.conf

# 添加如下内容 重要的事情说三遍,如果对外开发的服务器 不修改、不修改、不修改!
loopback_users=none

# 重启RabbitMQ
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app

8、新增账户配置管理权限

# 创建账户
rabbitmqctl add_user 用户名 密码

# 授予用户为管理员角色
rabbitmqctl set_user_tags 用户名 administrator

# 给用户授权
# "/"表示虚拟机
# ".*" ".*" ".*" 表示完整权限
rabbitmqctl set_permissions -p "/" 用户名 ".*" ".*" ".*"

2.3 Docker安装

1、拉取镜像

docker pull rabbitmq

2、启动RabbitMQ容器

docker run -d --hostname {自定义HOSTNAME} --name {rabbit名称} -p 15672:15672 -p 5672:5672 rabbitmq

3、开启 Web 界面管控台插件

# 查询rabbitmq容器ID
docker ps

# 进入容器
docker exec -it 容器ID /bin/bash

# 开启管控台插件
rabbitmq-plugins enable rabbitmq_management

# 新增用户名和密码 这里和linux 安装第8步骤一样
# 同时修改 rabbitmq 的配置文件 loopback_users 设置为 none loopback_users=none

# 退出容器
ctrl+p+q 

3.RabbitMQ的工作模式

RabbitMQ 其实一共有六种工作模式:简单模式(Simple)、工作队列模式(Work Queue)、发布订阅模式(Publish/Subscribe)、路由模式(Routing)、通配符模式(Topic)、远程调用模式(RPC)。其中发布订阅模式、路由模式、通配符模式这三种模型都属于订阅模式,只不过它们之间进行路由的方式不同罢了。远程调用模式是 RPC 不属于MQ,所以最终统计下来就是五种工作模式。这里,我们统一划分两类。

下面通过 demo 工程分别演示这五种模式。注意:所有的代码工程都是用 JAVA 原生来操作的。

创建 maven 项目,添加RabbitMQ 基础依赖

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.16.0</version>
    </dependency>
</dependencies>

创建 RabbitMQ 连接工具类,方便 demo 工程其他类方法调用获取连接对象

public class ConnectionFactoryUtil {
    public static Connection getConnection() throws IOException, TimeoutException {
        // 建立工厂连接
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 设置服务地址
        connectionFactory.setHost("127.0.0.1");
        // 设置端口
        connectionFactory.setPort(5672);
        // 设置连接登录账号
        connectionFactory.setUsername("logic");
        // 设置连接登录密码
        connectionFactory.setPassword("123456");
        // 设置虚拟主机机vhost 这里默认跟目录 "/"
        connectionFactory.setHost("/");

        // 建立连接
        Connection connection = connectionFactory.newConnection();
        return connection;
    }
}

3.1 简单模式 Simple

简单模式
结合示意图案例分析: RabbitMQ是一个消息代理,它接受和转发消息。我们可以把它抽象成一个货运仓库,当商家把商品打包放进仓库后,可以确定快递员最后一定会把快递送到收件人手里。

示意图解释: P(Producer / Publisher):生产者, 一个发送消息的用户应用程序。 C(Consumer): 消费者,一个用来等待接收消息的用户应用程序。 Queue(红色区域):消息队列,作用是接收消息、缓存消息,队列只受主机的内存和磁盘限制。生产者将消息发送到队列,队列是存储消息的缓冲区,消费者从队列中获取消息。 应用场景: 简单的发送与接收,没有特别的处理。

简单模式特点: 1、一个生产者对应一个消费者,通过队列进行消息传递。 2、使用默认的direct交换机。

工程实践: 一个生产者生产消息、一个消费者消费消息

生产者发送消息 Producer

public class Producer {
    // 定义队列名称
    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        // 1、建立工厂连接
        Connection connection = ConnectionFactoryUtil.getConnection();

        // 2、建立信道
        Channel channel = connection.createChannel();

        // 3、创建队列,如果队列存在,则使用该队列, 声明一个队列是幂等的 且只有当队列不存在时才会被创建
        /**
         * 参数1:队列名
         * 参数2:是否持久化,true表示MQ重启后队列还在。
         * 参数3:是否私有化,false表示所有消费者都可以访问,true表示只有第一次拥有它的消费者才能访问
         * 参数4:是否自动删除,true表示不再使用队列时自动删除队列(当没有消费者时,就自动删除)
         * 参数5:其他额外参数
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 4、发送消息
        String message = "Hello RabbitMQ";

        /**
         * 参数1:交换机名,""表示默认交换机
         * 参数2:路由键,简单模式就是队列名
         * 参数3:其他额外参数
         * 参数4:要传递的消息字节数组
         */
        channel.basicPublish("",QUEUE_NAME, null, message.getBytes());

        // 5、关闭信道和连接
        channel.close();
        connection.close();
        System.out.println("===消息发送成功===");
    }
}

发送成功
管控台
详情
消费者消费消息 Consumer

public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException {

        // 1、建立连接工厂
        Connection connection = ConnectionFactoryUtil.getConnection();

        // 2、建立信道
        Channel channel = connection.createChannel();

        // 3、监听队列
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                
                /*
                回调方法:当收到消息时,会自动执行该方法
                consumerTag: 标识
                envelope: 获取一些信息,如:交换机、路由key
                properties: 配置信息
                */
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("envelope:"+envelope);
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("接受消息为:" + message);
            }
        };

        /**
         * 参数1:监听的队列名
         * 参数2:是否自动签收,如果设置为false,则需要手动确认消息已收到,否则MQ会一直发送消息
         * 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
         */
        channel.basicConsume("simple_queue", true, consumer);
    }
}

执行消费者程序后,看下RabbitMQ管控台,发现消息队列的消息已经被消费而清空;且消费者输出结果后,其程序并不会停止而是一直等待监听生产者生产消息继续消费。

控制台
队列

3.2 工作队列模式 Work Queue

工作队列模式

说明: 与简单模式相比,工作队列模式(Work Queue)多了一些消费者,该模式也使用direct交换机,应用于处理消息较多的情况。 特点: 一个队列对应多个消费者,通过队列进行消息传递 一条消息只会被一个消费者消费 消息队列默认采用轮询的方式将消息平均发送给消费者 使用Rabbitmq默认交换机 direct 应用场景: 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。

生产者发送消息 Producer

public class Producer {
    // 定义队列名称
    private final static String QUEUE_NAME = "work_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1、建立工厂连接
        Connection connection = ConnectionFactoryUtil.getConnection();

        // 2、创建信道
        Channel channel = connection.createChannel();

        // 3、创建队列、并持久化
        channel.queueDeclare("work_queue", true, false, false, null);

        // 4、发送大量消息
        /**
         * 参数3:表示该消息为持久化消息,即除了保存到内存还会保存到磁盘中
         */
        for (int i = 0; i < 30; i++) {
            channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,
                    ("你好,你有新快递编号为:"+i).getBytes());
        }

        // 6、关闭资源
        channel.close();
        connection.close();
    }
}

Queue
消费者消费消息 Consumer 创建三个消费者:ConsumerOne、ConsumerTwo、ConsumerThree 因为三个消费者的代码大致相同,这里只贴ConsumerOne的代码,ConsumerTwo、ConsumerThree改下类文件和输出信息即可。

public class ConsumerOne {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1、建立连接工厂
        Connection connection = ConnectionFactoryUtil.getConnection();

        // 2、建立信道
        Channel channel = connection.createChannel();

        // 3、监听队列处理消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("消费者One 消费消息:" + message);
            }
        };
        
        channel.basicConsume("work_queue", true, consumer);
    }
}

结果
Queue
总结:
1、工作队列模式 使用了MessageProperties.PERSISTENT_TEXT_PLAIN 来设置消息持久化,目的是为了保证数据安全可靠不丢失。但是,事与愿违。消息虽然被标记为持久化却并不能完全保证消息不会丢失。尽管MessageProperties.PERSISTENT_TEXT_PLAIN 告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候;可能存在还没有存储完的情况,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。因此持久性保证并不强,进而需要引入发布确认、ACK。本篇幅不做详解。

2、多个消费者进行消息消费,因为消息是轮询平均发送给消费者。可能会有某个消费者Slow;因为要处理其他复杂的业务逻辑,其消费的效率相对其他消费者比较慢,这个就会照成当其他消费者已经消费完处于空闲状态时,因平均分配原则,队列任会继续把消息发给 Slow 处于忙碌状态,大大降低了系统的性能。正确的做法的是“能劳者多劳;消费越快的,让其消费的越多”。本篇幅不做详解。

3.3 发布队列模式 Publish/Subscribe

发布队列模式

特点:生产者将消息发送给交换机,交换机将消息转发到绑定此交换机的每个队列中;工作队列模式的交换机只能将消息发送给一个队列,发布订阅模式的交换机能将消息发送给多个队列。发布订阅模式使用 fanout 交换机。

生产者发送消息 Producer

public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1、建立工厂连接
        Connection connection = ConnectionFactoryUtil.getConnection();

        // 2、创建信道
        Channel channel = connection.createChannel();

        // 3、创建交换机
        /**
         * 参数1:交换机名
         * 参数2:交换机类型
         * 参数3:交换机持久化
         */
        channel.exchangeDeclare("exchange_fanout", BuiltinExchangeType.FANOUT,true);

        // 4、创建队列
        // 短信
        channel.queueDeclare("SEND_MESSAGE", true, false, false, null);
        // 邮件
        channel.queueDeclare("SEND_MAIL", true, false, false, null);
        // 站内信
        channel.queueDeclare("SEND_STATION", true, false, false, null);

        // 5、交换机绑定队列
        /**
         * 参数1:队列名
         * 参数2:交换机名
         * 参数3:路由关键字,发布订阅模式写 ""即可
         */
        channel.queueBind("SEND_MAIL", "exchange_fanout", "");

        channel.queueBind("SEND_MESSAGE", "exchange_fanout", "");

        channel.queueBind("SEND_STATION", "exchange_fanout", "");

        // 6、发送消息
        for (int i = 0; i < 10; i++) {
            channel.basicPublish("exchange_fanout", "", null, ("尊敬的Vip用户,秒杀商品开抢了!"+i).getBytes());
        }

        // 7、关闭资源
        channel.close();

        connection.close();
    }
}
消费者消费消息 Consumer

创建三个消费者:邮件消费者(ConsumerMail)、短信消费者(ConsumerMessage)、站内信消费者(ConsumerStation)

1、邮件消费者(ConsumerMail)

public class ConsumerMail {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1、建立工厂连接
        Connection connection = ConnectionFactoryUtil.getConnection();

        // 2、建立信道
        Channel channel = connection.createChannel();

        // 3、监听队列
        channel.basicConsume("SEND_MAIL", true, new DefaultConsumer(channel) {

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body,"UTF-8");
                System.out.println("接收邮件:"+message);
            }
        });
    }
}
2、短信消费者(ConsumerMessage)

public class ConsumerMessage {
    public static void main(String[] args) throws IOException, TimeoutException {
        ...
        // 3、监听队列
        channel.basicConsume("SEND_MESSAGE", true, new DefaultConsumer(channel) {

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body,"UTF-8");
                System.out.println("接收短信消息:"+message);
            }
        });
    }
}

站内信消费者(ConsumerStation)

public class ConsumerStation {
    public static void main(String[] args) throws IOException, TimeoutException {
        ...
        // 3、监听队列
        channel.basicConsume("SEND_STATION", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("接收站内信消息:"+message);
            }
        });
    }
}

代码写完以后,先启动生产者创建队列生产消息,将消息转发给交换机,再由交换机把消息发给绑定该交换机的队列中去,等待消费者获取消费。

Queue
可以看到启动生产者后,MQ创建了三个队列;每个队列中有10条待消费的消息(Ready、Total 都是10)。

再启动三个消费者来订阅队列,并监听等待生产者生产消息进行消费,如果队列中消息已经生产则会立刻获取消费。
结果
总结:订阅模式中,多个消费者同时订阅一个队列,该队列会轮询地把消息平均分配给每个消费者,这也就是标准的工作队列模式的模型。通过前面的 demo工程可知,我们在使用发布订阅模式时,所有消息都会发送到绑定的队列中,但很多时候,不是所有消息都无差别的发布到所有队列中,这无形当中就会照成不必要的资源浪费。为了解决这个问题,路由模式就诞生了。

3.4 路由模式 Routing

路由

说明:路由(Routing)模式是发布订阅模式的升级版。我们知道发布订阅模式是无条件地将所有消息分发给所有消费者队列,每个队列中都有相同的消息;路由模式,由上图很容易理解,每个队列消息会因为绑定的路由不同而不同。 特点:1、每个队列绑定一个路由关键字RoutingKey,生产者将带有RoutingKey的消息发送给交换机,交换机再根据路由 RoutingKey关键字将消息定向发送到指定的队列中;2、默认使用 direct 交换机。 应用场景:1、如在电商网站的促销活动中,双十一搞促销活动会把促销消息发布到所有队列中去;而一些小的促销活动为了节约成本,只发布到站内信队列。2、为了节省磁盘空间,需要将重要的错误消息引导到日志文件,同时仍然能够在控制台上打印输出所有日志消息。

demo工程用发布订阅模式的生产者和消费者进行改进升级演示。

生产者发送消息 Producer
public class Producer {
// 定义交换机名称
private final static String ROUTE_NAME = “exchange_routing”;

// 大促销路由 RoutingKey
private final static String BIG_ROUT_KEY = "big";

// 小促销路由 RoutingKey
private final static String SMALL_ROUT_KEY = "small";

public static void main(String[] args) throws IOException, TimeoutException {
    //  1、建立工厂连接
    Connection connection = ConnectionFactoryUtil.getConnection();

    // 2、创建信道
    Channel channel = connection.createChannel();

    // 3、建立交换机
    channel.exchangeDeclare("exchange_routing", BuiltinExchangeType.DIRECT, true);

    // 4、创建队列
    channel.queueDeclare("SEND_MESSAGE2", true, false, false, null);
    channel.queueDeclare("SEND_STATION2", true, false, false, null);
    channel.queueDeclare("SEND_EMAIL2", true, false, false, null);

    // 5、交换机通过 RoutingKey 关键字绑定队列
    channel.queueBind("SEND_MESSAGE2", ROUTE_NAME, BIG_ROUT_KEY);
    channel.queueBind("SEND_EMAIL2", ROUTE_NAME, BIG_ROUT_KEY);
    channel.queueBind("SEND_STATION2", ROUTE_NAME, BIG_ROUT_KEY);
    channel.queueBind("SEND_STATION2", ROUTE_NAME, SMALL_ROUT_KEY);

    // 6、发送消息
    channel.basicPublish(ROUTE_NAME, BIG_ROUT_KEY, null, ("双十一大促销活动--全场买一送一").getBytes());
    channel.basicPublish(ROUTE_NAME, SMALL_ROUT_KEY, null, ("小促销活动--满1000立减200").getBytes());

    // 7、关闭资源
    channel.close();
    connection.close();
}

}

创建三个消费者:邮件消费者(ConsumerMail)、短信消费者(ConsumerMessage)、站内信消费者(ConsumerStation);直接用 发布订阅模式的代码,修改下监听的队列即可 。

邮件消费者(ConsumerMail)

public class ConsumerMail {
    public static void main(String[] args) throws IOException, TimeoutException {
        ...
        // 3、监听队列 SEND_MAIL2
        channel.basicConsume("SEND_MAIL2", true, new DefaultConsumer(channel) {
            ...
        });
    }
}

短信消费者(ConsumerMessage)

public class ConsumerMessage {
    public static void main(String[] args) throws IOException, TimeoutException {
        ...
        // 3、监听队列 SEND_MESSAGE2
        channel.basicConsume("SEND_MESSAGE2", true, new DefaultConsumer(channel) {
            ...
        });
    }
}

站内信消费者(ConsumerStation)

public class ConsumerMessage {
    public static void main(String[] args) throws IOException, TimeoutException {
        ...
        // 3、监听队列 SEND_STATION2
        channel.basicConsume("SEND_STATION2", true, new DefaultConsumer(channel) {
            ...
        });
    }
}

代码编写完毕开始运行 demo 工程;依次运行生产者(生成队列、生产消息)、三个消费者(监听队列消费消息)。

生产者业务流程执行的结果如下:

结果
MQ 管控台界面 SEND_STATION2队列 有两条消息 “双十一大促销活动–全场买一送一” 和 “小促销活动–满1000立减200”,另外两个则各自只有一条消息;因此对于当前消费者而言,其执行结果站内信消费者会消费两条消息,其他有且只有一条。

总结:路由模式是一种精准的匹配,只有设置了 Routing Key 后消息才能进行分发。但是,我们在实际的工作中开发需求时还有一些非常模糊的情况,比如消费者存在某种交集的联系只要符合某一个条件,就需要有消息分发获取消息处理业务逻辑。这时候就需要用到“通配符模式“。

3.5 通配符模式 Topics

说明:通配符模式(Topic)是在路由模式的基础上升级,给队列绑定带通配符的路由关键字,只要消息的RoutingKey 能实现通配符匹配而不再是固定的字符串,就会将消息转发到该队列。通配符模式比路由模式更灵活。 特点:1、消息设置RoutingKey时,RoutingKey由多个单词构成,中间以 . 分割。2、队列设置RoutingKey时,#可以匹配任意多个单词,*可以匹配任意一个单词。3、使用 topic 交换机。 应用场景:通配符模式的匹配规则相对于路由模式要显得抽象,比如工厂生产手机屏,虽然手机屏的品牌(vivo、华为、荣耀)有很多,但是只要有手机屏产出就会发送一条消息给厂商,时刻统计手机屏的数量。路由模式方案:每个手机品牌独占一个队列和交换机绑定一个唯一标识 RoutingKey 手机品牌;通配符模式:一个队列和交换机绑定一个通配的标识 RoutingKey 即可。

通配符规则:# 匹配一个或多个词,* 匹配有且仅有1个词。

发送消息匹配成功的条件
#.AA的左边可以任意多个单词,右边不可以有单词
A.#A的右边可以任意多个单词,左边不可以有单词
#.A.#A的左边和右边都可以任意多个单词
*.AA的左边有且只能有一个单词,右边不可以有单词
A.*A的右边有且只能有一个单词,左边不可以有单词
.A.A的左边和右边都有且只能有一个单词

demo工程用路由模式的生产者和消费者进行改进升级演示。

生产者发送消息 Producer

public class Producer {
    private final static String ROUTE_NAME = "exchange_topic";
    
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1、建立工厂连接
        Connection connection = ConnectionFactoryUtil.getConnection();

        // 2、建立信道
        Channel channel = connection.createChannel();

        // 3、建立交换机
        channel.exchangeDeclare("exchange_topic", BuiltinExchangeType.TOPIC, true);

        // 4、创建队列
        channel.queueDeclare("SEND_MAIL3", true, false, false, null);
        channel.queueDeclare("SEND_MESSAGE3", true, false, false, null);
        channel.queueDeclare("SEND_STATION3", true, false, false, null);

        // 5、交换机绑定队列
        channel.queueBind("SEND_MAIL3",ROUTE_NAME,"#.big.#");
        channel.queueBind("SEND_MESSAGE3",ROUTE_NAME,"#.middle.#");
        channel.queueBind("SEND_STATION3",ROUTE_NAME,"#.small.#");

        // 6、发送消息
        channel.basicPublish(ROUTE_NAME, "big.middle", null, ("双十一大促销活动--全场买一送一").getBytes());
        channel.basicPublish(ROUTE_NAME, "small",null, ("小促销活动--满1000立减200").getBytes());

        // 7、关闭资源
        channel.close();
        connection.close();

    }
}

生产者对邮箱和短信发送"大促销活动"、站内信只发送"小促销活动"。

消费者的代码和路由模式一样就不再贴出来了,只需要修改监控的队列名称即可。

4.SpringBoot整合RabbitMQ

前面用Java 原生来实现了 RabbitMQ的五种工作模型的演示工程,整个操作过程比较繁琐 ,目的是为了我们能够非常清楚地认识 RabbitMQ 工作流程以及掌握它的使用方法。而真正在工作中是不会使用这种原生的方式来实现消息推送和消费服务(没有意外除非很特殊)。那么为了简化我们的代码编写,本次开始用 SpringBoot 来整合 RabbitMQ。

在编写代码前,我们都知道 RabbitMQ 作为消息中间的使用本质功能就是一个生产者生产消息和一个消费者消费消息。那么一般情况下,生产者和消费者是不会在同一个项目中,如果在同一个项目中就不需要使用 MQ,直接项目内部方法调用即可。所以,为了真实复现我们开发中项目部署和架构方案,demo 工程创建两个 Maven 项目,一个生产者 provider (生成消息)、一个消费者 consumer(消费消息),模型选用通配符模式(topic)。

注意:为了简化粘贴出来的代码,重复的代码和代码上下文结构不做重点解释的以三个点"…"进行省略。

一、首先创建 springBootProdiverRabbitMq 项目
springboot
1、设置 JDK 版本信息、添加项目所需的 jar 依赖
修改 pom.xml

...
<properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

    <dependencies>
        <!-- rabbitmq 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
...

2、编写配置文件 application.yml

server:
  # 项目访问的端口号
  port: 9001
spring:
  application:
    # 项目名称
    name: provider-rabbitmq
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /

#日志格式
logging:
  pattern:
    console: '%d{HH:mm:ss.SSS} %clr(%-5level) ---  [%-15thread] %cyan(%-50logger{50}):%msg%n'

3、编写配置类绑定交换机和队列的控制器放到 Spring 容器里

@Configuration
public class RabbitConfig {
    // 指定交换机名称
    private final String EXCHANGE_NAME = "boot_topic_exchange";

    // 指定队列名
    private final String QUEUE_NAME = "boot_queue";

    // 创建交换机
    @Bean("bootExchange")
    public Exchange getExchange() {
         return ExchangeBuilder
                 .topicExchange(EXCHANGE_NAME)
                 .durable(true) // 是否持久化
                 .build();
    }

    // 创建队列
    @Bean("bootQueue")
    public Queue getMessageQueue() {
        return new Queue(QUEUE_NAME);
    }

    // 创建交换机绑定队列
    @Bean
    public Binding bindingExchangeQueue(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.happyNewYear.#")   // 通配符模式 要匹配的路由键 RoutingKey
                .noargs();
    }
}

4、编写生产者生产消息

@RestController
@RequestMapping("bootRabbitMq")
public class RabbitProvider {
    // 注入 RabbitTemplate 工具类
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendNewYearMessage")
    public String sendMessage() {
        String messageId = String.valueOf(UUID.randomUUID());   // 随机一个消息 ID
        String messageContent = "快过年了,提前祝你新年快乐。";   // 消息主题内容
        String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        HashMap<String, Object> messageMap = new HashMap<>();
        messageMap.put("messageId", messageId);
        messageMap.put("messageContent", messageContent);
        messageMap.put("sendTime", sendTime);

        /*
         * 发送消息
         * 参数1:交换机名称
         * 参数2:路由 routeKey
         * 参数3:消息主题内容
         */
        rabbitTemplate.convertAndSend("boot_topic_exchange", "happyNewYear", messageMap);
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";
    }
}

5、编辑启动器

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

6、启动 SpringBoot 后 打开浏览器窗口执行
结果
二、创建 springBootConsumerRabbitMq 项目

消费者
1、设置 JDK 版本信息、添加项目所需的 jar 依赖;pom.xml 与 springBootProdiverRabbitMq 项目一致,这里就不再重复贴代码了

2、编写配置文件 application.yml 与 springBootProdiverRabbitMq 项目一致,唯一不同的是 项目访问的端口号和项目名称

...
server:
  # 项目访问的端口号
  port: 9002
spring:
  application:
    # 项目名称
    name: consumer-rabbitmq
...

3、编写消费者

@Component
@RabbitListener(queues = "boot_queue")  // 监听队列
public class RabbitConsumer {

    @RabbitHandler  // @RabbitListener 当有收到消息的时候,就交给 @RabbitHandler 的方法处理,根据接受的参数类型进入具体的方法中。
    public void listenMessage(Map messageContent) {
        System.out.println("topicReceiver消费者收到新年祝福:" + messageContent.toString());
    }
}

4、编辑启动器 与 springBootProdiverRabbitMq 项目一致
5、启动 SpringBoot
消费者自动监听队列当队列里有消息时就会消费
结果

5.消息的可靠性传递

经过前面的学习,我们知道 RabbitMQ 消息投递路径是:
路径
可以看到在这个过程中,每个环节都有可能因某些故障导致消息传递失败,在项目开发出现这种消息无法传递的情况,对系统数据支撑的安全和可靠是致命的。所以如何才能保证 MQ 消息可靠、无误、准确地传递是本篇章的重点。

从图中可以总结出以下几点:
1、确认模式(confirm):可以监听消息是否从生产者成功传递到交换。
2、退回模式(return):可以监听消息是否从交换机成功传递到队列。
3、消费者消息确认(Ack):可以监听消费者是否成功处理消息。

在分解这三块的消息传递前,先准备两个SpringBoot项目,分别是生产者(reliableProvider)和消费者(reliableConsumer)。
两个项目的pom.xml 配置文件内容一样,具体配置和版本依赖可见上一篇的 “SpringBoot 整合 RabbitMQ 的篇章“,这里不再过多详细描述。规定约束好生产者的端口号为 9003;消费者的端口号为 9004。

注意:为了简化粘贴出来的代码,重复的代码和代码上下文结构不做重点解释的以三个点"…"进行省略。

生产者:
1、生产者 application.yml 配置文件:

server:
  # 项目访问的端口号
  port: 9003
spring:
  application:
    # 项目名称
    name: reliable-provider
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /

#日志格式
logging:
  pattern:
    console: '%d{HH:mm:ss.SSS} %clr(%-5level) ---  [%-15thread] %cyan(%-50logger{50}):%msg%n'

2、添加生成者的配置类 RabbitConfig.java 创建交换机和队列的绑定加入到 Spring 容器里。

@Configuration
public class RabbitConfig {
    // 指定交换机名称
    private final String EXCHANGE_NAME = "reliable_exchange";

    // 指定队列名
    private final String QUEUE_NAME = "reliable_queue";

    // 创建交换机
    @Bean("bootExchange")
    public Exchange getExchange() {
         return ExchangeBuilder
                 .topicExchange(EXCHANGE_NAME)
                 .durable(true) // 是否持久化
                 .build();
    }

    // 创建队列
    @Bean("bootQueue")
    public Queue getMessageQueue() {
        return new Queue(QUEUE_NAME);
    }

    // 创建交换机绑定队列
    @Bean
    public Binding bindingExchangeQueue(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.happyNewYear.#")   // 通配符模式 要匹配的路由键 RoutingKey
                .noargs();
    }
}

3、创建编写启动类 SpringBootApp.java

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

4、创建生产者消息的发送的类 ReliableProvider.java

@RestController
@RequestMapping("provider")
public class ReliableProvider {
    // 注入 RabbitTemplate 工具类
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendNewYearMessage")
    public String sendMessage() {
        String messageId = String.valueOf(UUID.randomUUID());   // 随机一个消息 ID
        String messageContent = "快过年了,提前祝你新年快乐。";   // 消息主题内容
        String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        HashMap<String, Object> messageMap = new HashMap<>();
        messageMap.put("messageId", messageId);
        messageMap.put("messageContent", messageContent);
        messageMap.put("sendTime", sendTime);

        /*
         * 发送消息
         * 参数1:交换机名称
         * 参数2:路由 routeKey
         * 参数3:消息主题内容
         */
        rabbitTemplate.convertAndSend("reliable_exchange", "happyNewYear", messageMap);
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";
    }
}

5、启动 生产者服务,成功后在浏览器上输入生产消息并 查看RabbitMQ 管控台是否有消息生成
Queue
到此,生产者的项目就搭建完成了。

前面的创建步骤完成,且能生产者能正常发送消息以后,下面就开始正题 “确认模式”。

首先假如没有确认模式,当生产者发送消息到交换机,发送期间或因某种不可控因素又或者是交换机故障等等。消息并没有正在成功发送到交换机上,此时,消息创建就会失败。但是,这个过程用户是无感知的,程序员也不知道是什么故障情况。如果在生产者把消息推到交换机上这个过程做个监听,是不是就一目了然了呢。首先我们清空下当前队列 reliable_queue 已经存在的数据,然后在生产者这个类,把发送消息改成一个不存在的交换机,然后再看下运行结果。

...
// rabbitTemplate.convertAndSend("reliable_exchange", "happyNewYear", messageMap);
// 模拟确认模式 使交换机故障让消息无法发送到交换机上
rabbitTemplate.convertAndSend("xxxx", "happyNewYear", messageMap);
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";
...

重启服务后,浏览器访问下生产者地址,在RabbitMQ 控制台就会发现没有消息产出了。

下面就开始实施部署 “确认模式” 的代码来启动对生产者发送消息到交换机的监控。

5.1确认模式

1、修改生产者配置 application.yml 文件开启确认模式

...
spring:
  ...
  # 配置RabbitMQ
  rabbitmq:
    ...
    virtual-host: /
    # 开启确认模式
    publisher-confirm-type: correlated

2、配置编写确认模式回调函数 RabbitConfirmConfig.java

@Configuration
public class RabbitConfirmConfig {
    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        // 设置开启 Mandatory 强制执行调用回调函数
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * 被调用的回调方法
             * @param correlationData 相关配置信息
             * @param ack 交换机是否成功收到消息 可以根据 ack 做相关的业务逻辑处理
             * @param cause 失败原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack) {
                    // 控制台 打印输出内容
                    System.out.println("生产者已成功将消息推送到交换机");
                } else {
                    // 控制台 打印输出内容
                    System.out.println("ConfirmInfo:     "+"相关配置信息: "+correlationData);
                    System.out.println("ConfirmInfo:     "+"确认结果: "+ack);
                    System.out.println("ConfirmInfo:     "+"原因: "+cause);
                    
                    // 可做针对性地业务逻辑处理,例如:让消息重发、发送邮件通知程序员、做日志等等。
                }
            }
        });

        return rabbitTemplate;
    }
}

3、重启服务发起生产者生产消息的请求、并观察控制台

生产者推送信息到交换失败的情况:
结果
测试完成以后,把交换机改回正确的 reliable_exchange 重启服务再发起请求、观察控制台

...
rabbitTemplate.convertAndSend("reliable_exchange", "happyNewYear", messageMap);
// 模拟确认模式 使交换机故障让消息无法发送到交换机上
//rabbitTemplate.convertAndSend("xxxx", "happyNewYear", messageMap);
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";

生产者推送信息到交换机成功的情况:
结果
到此,确认模式的代码部署已完成,当生产者推送消息到交换机失败时,可以马上进行监控,并把结果反馈到负责人手里,进行排查处理解决问题。但是,我们从前文已经了解到光有确认模式是不足以保证消息从生产者到消费者的传输与消费都是成功的。从交换机拿到消息后,再次把消息发送给队列,这个过程中也是会有失败风险。这就引入了接下来需要部署的“退回模式” 用于监听消息从交换机到队列的传输结果。

5.2 退回模式

1、修改生产者配置 application.yml 文件开启确认模式

...
spring:
  ...
  # 配置RabbitMQ
  rabbitmq:
    ...
    # 开启确认模式
    publisher-confirm-type: correlated
    # 开启回退模式
    publisher-returns: true

2、配置编写退回模式回调函数 RabbitConfirmConfig.java

import org.springframework.amqp.core.ReturnedMessage;
...
@Configuration
public class RabbitConfirmConfig {
    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
        ...
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            ...
        });

        // 新增退回模式回调函数
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            /**
             * 被调用的回调方法
             * @param returnedMessage 消息主题内容对象
             */
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println("ReturnInfo:     "+"消息对象:"+returnedMessage.getMessage());
                System.out.println("ReturnInfo:     "+"错误码:"+returnedMessage.getReplyCode());
                System.out.println("ReturnInfo:     "+"错误信息:"+returnedMessage.getReplyText());
                System.out.println("ReturnInfo:     "+"交换机:"+returnedMessage.getExchange());
                System.out.println("ReturnInfo:     "+"路由键:"+returnedMessage.getRoutingKey());
               
               // 可做针对性地业务逻辑处理,例如:发送邮件通知程序员、做日志等等。
            }
        });

        return rabbitTemplate;
    }
}

3、模拟交换机推送消息到队列错误测试,把生产者 ReliableProvider.java 路由键改成 xxxx

...
// 模拟确认模式 使交换机故障让消息无法发送到交换机上
// rabbitTemplate.convertAndSend("xxxx", "happyNewYear", messageMap);
// 模拟退回模式 修改不存在的路由键 使交换机无法通过路由键把消息发送到队列中
rabbitTemplate.convertAndSend("reliable_exchange", "xxxx", messageMap);
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";
...

4、重启服务发起生产者生产消息的请求、并观察控制台
结果
需要注意的是退回模式,只有当出现交换机向队列发送消息失败时才回执行 ReturnsCallback() 函数的回调,与确认模式不通,确认模式无论成功与否都会执行 ConfirmCallback() 函数回调再通过 布尔类型的 ack 判断消息发送结果是成功还是失败。

5.3 ACK模式

在RabbitMQ中,消费者接收到消息后会向队列发送确认签收的消息,只有确认签收的消息才会被移除队列。这种机制称为消费者消息确认(Consumer Acknowledge,简称Ack)。类似快递员派送快递也需要我们签收,否则一直存在于快递公司的系统中。消息分为自动确认和手动确认。自动确认指消息只要被消费者接收到,无论是否成功处理消息,则自动签收,并将消息从队列中移除。但是在实际开发中,收到消息后可能业务处理出现异常,那么消息就会丢失。此时需要设置手动签收,即在业务处理成功再通知签收消息,如果出现异常,则拒签消息,让消息依然保留在队列当中。

5.3.1 自动确认

前面所有的 demo 工程都是自动确认,这里就不再贴代码演示了。“自动确认模式”对消费者而言不管消费者是否成功处理本次消息的投递,都会自动认为本次投递已经被正确处理,消息会被移除该队列。所以对于这种情况,如果消费者处理消费逻辑时抛出异常,此时的消费者其实是并没有真正把此消息处理成功,这样的结果就相当于丢失了此消息。对于这种情况一般我们都是使用try catch捕获异常后,记录日志来追踪数据,这样找出对应数据后再做后续的业务处理。但是,这种处理方式效率明显很低。

5.3.2 手动确认

手动确认有以下几种模式:
basicAck():用于肯定确认。
basicNack():用于拒绝确认。
basicReject():用于拒绝确认。

下面开始构建 demo 工程 reliableConsumer ,这里我们使用两种编写代码的方式来完成 Ack模式的部署,以下部署的代码先不模拟异常情况拒签场景。

第一种方式 application.yml 的配置方式:

1、消费者 application.yml 配置文件,并开启手动签收:

server:
  # 项目访问的端口号
  port: 9004
spring:
  application:
    # 项目名称
    name: reliable-consumer
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /
    # 开启手动签收
    listener:
      simple:
        acknowledge-mode: manual

#日志格式
logging:
  pattern:
    console: '%d{HH:mm:ss.SSS} %clr(%-5level) ---  [%-15thread] %cyan(%-50logger{50}):%msg%n'

2、创建消费者类 RabbitConsumerOfDeploy.java 编写消费者逻辑代码

@Component
public class RabbitConsumerOfDeploy {
    // 监听队列
    @RabbitListener(queues = "reliable_queue")
    public void listenMessage(Message message, Channel channel) throws Exception {
        // 消息投递序号,消息每次投递该值都会+1
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        byte[] body = message.getBody();
        try {
            /**
             * 签收消息
             * 参数1:消息投递序号
             * 参数2:是否一次可以签收多条消息,true:是;false:否
             */
            channel.basicAck(deliveryTag, true);
            ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
            Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
            String messageId = msgMap.get("messageId");
            String messageData = msgMap.get("messageContent");
            String createTime = msgMap.get("sendTime");
            objectInputStream.close();
            System.out.println("rabbitConsumer:  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
            System.out.println("消费的队列名:"+message.getMessageProperties().getConsumerQueue());
        } catch (Exception e) {
            /**
             * 拒签消息
             * 参数1:消息投递序号
             * 参数2:是否一次可以拒签多条消息,true:是;false:否
             * 参数3:拒签后消息是否重回队列,true:重回;false:不重回
             */
            System.out.println("消息消费失败!");
            channel.basicNack(deliveryTag, true, true);
            e.printStackTrace();
        }
    }
}

第二种方式 实现 ChannelAwareMessageListener 接口(和RabbitConsumerOfDeploy.java 有很多的相似处) :
1、编写 RabbitConsumer.java

@Component
public class RabbitConsumer implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        // 消息投递序号,消息每次投递该值都会+1
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            byte[] body = message.getBody();
            ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
            Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
            String messageId = msgMap.get("messageId");
            String messageData = msgMap.get("messageContent");
            String createTime = msgMap.get("sendTime");
            objectInputStream.close();
            System.out.println("rabbitConsumer:  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
            System.out.println("消费的队列名:"+message.getMessageProperties().getConsumerQueue());
            /**
             * 签收消息
             * 参数1:消息投递序号
             * 参数2:是否一次可以签收多条消息,true:是;false:否
             */
            channel.basicAck(deliveryTag, true);
        }catch (Exception e) {
            /**
             * 拒签消息
             * 参数1:消息投递序号
             * 参数2:是否一次可以拒签多条消息,true:是;false:否
             * 参数3:拒签后消息是否重回队列,true:重回队列;false:不重回队列
             */
            System.out.println("消息消费失败!");
            channel.basicNack(deliveryTag, true, true);
            e.printStackTrace();
        }
    }
}

2、编写Ack配置类 AckConfig.java 注入到容器的配置方式:

@Configuration
public class AckConfig {
    @Autowired
    private CachingConnectionFactory connectionFactory;

    @Autowired
    private RabbitConsumer rabbitConsumer;

    @Bean
    public SimpleMessageListenerContainer simpleMessageListenerContainer() {
        SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(connectionFactory);
        // 设置消费者个数,当前设置为1
        listenerContainer.setConcurrentConsumers(1);
        listenerContainer.setMaxConcurrentConsumers(1);
        listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        // 设置一个已存在的队列
        listenerContainer.setQueueNames("reliable_queue");

        listenerContainer.setMessageListener(rabbitConsumer);

        return listenerContainer;
    }
}

3、注释掉 配置类 application.yml 的手动签收配置

server:
  ...
spring:
  ...
  rabbitmq:
   ...
    # 开启手动签收
    # listener:
      # simple:
        # acknowledge-mode: manual

结果
接下来我们使用第二种方式来是模拟异常实现 ACK 手动确认的拒签场景

1、修改 RabbitConsumer.java

...
try {
    int i = 1/0; //模拟处理消息出现 触发ACK
    ...
}catch (Exception e) {
    ...
}

2、生产者生成一条消息并重启消费者服务

生产消息
经过异常模拟执行后,很清晰地看出当消费者消费出现异常时,RabbitMQ 控制台则拒签消息,让消息重回队列中进行下一次消费。该消息就会一直处于这样 Unacked 状态进行循环下去(消费-入列-消费-入列)。除非异常解除后才会停止循环,开始正常消费消息。当我们停止消费者时,消息会重新发放,Unacked 变为0,Ready 变为1。

需要注意的是:手动Ack如果处理方式不对会发生一些问题。

1.没有及时ack,或者程序出现bug,所有的消息将被存在unacked中,消耗内存如果忘记了ack,那么后果很严重。当Consumer退出时,Message会重新分发。然后RabbitMQ会占用越来越多的内存,由于 RabbitMQ会长时间运行,因此这个“内存泄漏”是致命的。

2.如果使用basicNack,将消费失败的消息重新塞进队列的头部,则会造成死循环。(解决basicNack造成的消息循环循环消费的办法是为队列设置“回退队列”,设置回退队列和阀值,如设置队列为q1,阀值为2,则在rollback两次后将消息转入q1)

综上,对于手动ack的使用注意以下三点:

1、在消费者端一定要进行ack,或者是nack,可以放在try方法块的finally中执行
2、可以对消费者的异常状态进行捕捉,根据异常类型选择ack,或者nack抛弃消息,nack再次尝试
3、对于nack的再次尝试,是进入到队列头的,如果一直是失败的状态,将会造成阻塞。所以最好是专门投递到“死信队列”

6.RabbitMQ高级特性

RabbitMQ 高级特性包括包括消息限流、实现限流不公平分发、消息存活时间、队列的优先级。因为有这些高级特性的存在使得它在不同的应用场景里面对各种问题都能有一个比较好的解决方案。例如:生产者生产大量的消息,但是消费者获取消息后要处理相关业务需要一定的时间,并不能很快地去消费这些大量的消息;此时,这些大数据量的消息就会冲击消费者,照成消费端处理业务达到瓶颈而崩溃,为了解决这个问题,可以把消息暂时存放在MQ当中,然后再按照一定的速度把消息发送给消费者来保护消费端正常处理业务。
示例图

6.1消费限流

SpringBoot 整合RabbitMQ的 pom.xml 文件依赖一致

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

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

<dependencies>
    <!-- rabbitmq 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

构建部署生产者 interdictProvider 项目
①、添加配置文件 application.yml

server:
  # 项目访问的端口号
  port: 9006
spring:
  application:
    # 项目名称
    name: reliable-provider
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /
    # 开启确认模式
    publisher-confirm-type: correlated
    # 开启回退模式
    publisher-returns: true

②、创建配置文件 RabbitConfig.java 编写交换机、队列的绑定加入到 Spring 容器中

@Configuration
public class RabbitConfig {
    // 指定交换机名称
    private final String EXCHANGE_NAME = "interdict_exchange";

    // 指定队列名
    private final String QUEUE_NAME = "interdict_queue";

    // 创建交换机
    @Bean("bootExchange")
    public Exchange getExchange() {
         return ExchangeBuilder
                 .topicExchange(EXCHANGE_NAME)
                 .durable(true) // 是否持久化
                 .build();
    }

    // 创建队列
    @Bean("bootQueue")
    public Queue getMessageQueue() {
        return new Queue(QUEUE_NAME);
    }

    // 创建交换机绑定队列
    @Bean
    public Binding bindingExchangeQueue(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.happyNewYear.#")   // 通配符模式 要匹配的路由键 RoutingKey
                .noargs();
    }
}

③、编写拥有确认模式和退回模式的配置文件 RabbitConfirmConfig.java

@Configuration
public class RabbitConfirmConfig {
    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        // 设置开启 Mandatory 强制执行调用回调函数
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * 被调用的回调方法
             * @param correlationData 相关配置信息
             * @param ack 交换机是否成功收到消息 可以根据 ack 做相关的业务逻辑处理
             * @param cause 失败原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack) {
                    // 控制台 打印输出内容
                    System.out.println("生产者已成功将消息推送到交换机");
                } else {
                    // 控制台 打印输出内容
                    System.out.println("ConfirmInfo:     "+"相关配置信息: "+correlationData);
                    System.out.println("ConfirmInfo:     "+"确认结果: "+ack);
                    System.out.println("ConfirmInfo:     "+"原因: "+cause);

                    // 可做针对性地业务逻辑处理,例如:让消息重发、发送邮件通知程序员、做日志等等。
                }
            }
        });

        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            /**
             * 消息发送成功时,被调用的回调方法
             * @param returnedMessage 消息主题内容对象
             */
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println("ReturnInfo:     "+"消息对象:"+returnedMessage.getMessage());
                System.out.println("ReturnInfo:     "+"错误码:"+returnedMessage.getReplyCode());
                System.out.println("ReturnInfo:     "+"错误信息:"+returnedMessage.getReplyText());
                System.out.println("ReturnInfo:     "+"交换机:"+returnedMessage.getExchange());
                System.out.println("ReturnInfo:     "+"路由键:"+returnedMessage.getRoutingKey());

                // 可做针对性地业务逻辑处理,例如:发送邮件通知程序员、做日志等等。
            }
        });

        return rabbitTemplate;
    }
}

④、编写生产者代码生产100条消息

@RestController
@RequestMapping("provider")
public class interdictProvider {
    // 注入 RabbitTemplate 工具类
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendNewYearMessage")
    public String sendMessage() {
        int m=0;
        for (int i = 1; i <= 100; i++) {
            String messageId = String.valueOf(UUID.randomUUID());   // 随机一个消息 ID
            String messageContent = "快过年了,提前祝你新年快乐。第 "+i+"封信";   // 消息主题内容
            String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            HashMap<String, Object> messageMap = new HashMap<>();
            messageMap.put("messageId", messageId);
            messageMap.put("messageContent", messageContent);
            messageMap.put("sendTime", sendTime);

            /*
             * 发送消息
             * 参数1:交换机名称
             * 参数2:路由 routeKey
             * 参数3:消息主题内容
             */
            rabbitTemplate.convertAndSend("interdict_exchange", "happyNewYear", messageMap);
            m++;
        }
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"封新年祝福信已发送</p>";
    }
}

⑤、启动测试生产者,浏览器 请求 生成100消息到 MQ

结果
构建部署消费者 interdictConsumer 项目

①、添加配置文件 application.yml

spring:
  application:
    # 项目名称
    name: reliable-consumer
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /
    # 消费者必须开启手动签收
    listener:
      simple:
        acknowledge-mode: manual

②、编写消费者

@Component
public class InterdictConsumer {
    @RabbitListener(queues = "reliable_queue")
    public void listenMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        byte[] body = message.getBody();
        try {
            /**
             * 签收消息
             * 参数1:消息投递序号
             * 参数2:是否一次可以签收多条消息,true:是;false:否
             */
            channel.basicAck(deliveryTag, true);
            ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
            Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
            String messageId = msgMap.get("messageId");
            String messageData = msgMap.get("messageContent");
            String createTime = msgMap.get("sendTime");
            objectInputStream.close();
            System.out.println("rabbitConsumer:  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
            System.out.println("消费的队列名:"+message.getMessageProperties().getConsumerQueue());

            /**模拟接收消息,所需处理时间 2秒**/
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            /**
             * 拒签消息
             * 参数1:消息投递序号
             * 参数2:是否一次可以拒签多条消息,true:是;false:否
             * 参数3:拒签后消息是否重回队列,true:重回;false:不重回
             */
            System.out.println("消息消费失败!");
            channel.basicNack(deliveryTag, true, true);
            e.printStackTrace();
        }
    }
}

演示在没有限流的情况下,消费端消费消息的状态。
结果
可以看到所有消息都会堆积到 Unacked 未签收中。如果存在当有大量消息产出并堆积到消费者,很可能就会照成内存溢出或泄露导致系统瘫痪而不可用。如何解决这个问题呢?那就需要需要用到消费端限流机制。

2、配置限流机制

修改配置文件 application.yml
添加配置 spring.rabbitmq.listener.simple.prefetch 为 10

spring:
  application:
    # 项目名称
    name: reliable-consumer
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /
    # 消费者必须开启手动签收
    listener:
      simple:
        # 限流机制必须开启手动签收
        acknowledge-mode: manual
        # 消费者最多拉取10条消息进行消费,当签收后不满10条则继续拉取消息
        prefetch: 10

总结:当开启限流后,不会有大量消息堆积到消费端;然当有大量消息进来时,有且只有一定量 prefetch 设定数量值的消息堆在 Unacked 中,当签收后 Unacked 中的值不满 prefetch 设定值时就会自动拉取(演示的案例中,Unacked 会一直保持消息条数的数值为10的状态),直至消息消费完;充分保护了消费端正常运行签收消费消息。

6.2消费不公平分发

说到不公平分发之前,先再简单了解下RabbitMQ的消息推送过程,首先生产者生产消息并把消息发送到交换机;再由交换机根据路由规则等推送到对应的队列中;最后再由队列轮询平均发送给每个消费者。

1、在限流 Demo 工程基础之上新增一个消费者 InterdictTwoConsumer.java 命名为“2号消费者”并监听 interdict_queue 队列,模拟处理消费消息的能力为每1秒处理一个消息。

@Component
public class InterdictTwoConsumer {
    @RabbitListener(queues = "interdict_queue")
    public void listenMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        byte[] body = message.getBody();
        try {
            /**
             * 签收消息
             * 参数1:消息投递序号
             * 参数2:是否一次可以签收多条消息,true:是;false:否
             */
            channel.basicAck(deliveryTag, true);
            ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
            Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
            String messageId = msgMap.get("messageId");
            String messageData = msgMap.get("messageContent");
            String createTime = msgMap.get("sendTime");
            objectInputStream.close();
            System.out.println("rabbitConsumer:  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
            System.out.println("2号消费者,消费的队列名:"+message.getMessageProperties().getConsumerQueue());

            /**模拟接收消息,所需处理时间 1秒**/
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            /**
             * 拒签消息
             * 参数1:消息投递序号
             * 参数2:是否一次可以拒签多条消息,true:是;false:否
             * 参数3:拒签后消息是否重回队列,true:重回;false:不重回
             */
            System.out.println("消息消费失败!");
            channel.basicNack(deliveryTag, true, true);
            e.printStackTrace();
        }
    }
}

2、修改消费者 InterdictConsumer.java 注释模拟处理消息的时间

...
System.out.println("rabbitConsumer:  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
System.out.println("消费的队列名:"+message.getMessageProperties().getConsumerQueue());

/**模拟接收消息,所需处理时间 2秒**/
// TimeUnit.SECONDS.sleep(2);
...

3、注释配置文件 application.yml 的 prefetch 签收参数值

spring:
  ...
  rabbitmq:
    ...
    listener:
      simple:
        # 限流机制必须开启手动签收
        acknowledge-mode: manual
        # 消费者最多拉取10条消息进行消费,当签收后不满10条则继续拉取消息
        # prefetch: 10
...

4、重启消费者启动器,浏览器调用生产者生成消息,观察控制台
因为2号消费者处理消息比较慢,前消费者消费完后处于空闲状态一直在等待2号消费者。
修改配置文件 application.yml,只需要将prefetch 参数修改为1 即可。

spring:
  application:
    # 项目名称
    name: reliable-consumer
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /
    # 消费者必须开启手动签收
    listener:
      simple:
        # 限流机制必须开启手动签收
        acknowledge-mode: manual
        # 消费者最多拉取1条消息进行消费
        prefetch: 1

原理是:队列阻塞,只给每个消费者发送一条消息,当某个消费者消费完后立刻获取新的消息。

通过控制台可以看到 前消费者消费了大量消息,2号消费者仅仅消费了极少量的消息,但,整个系统的处理消息的能力大大提升。

6.3 消息存活时间

在某些应用场景中,某些消息只是临时通知的但却一直没有被消费者消费,这种消息就会一直存在MQ当中,浪费资源空间。这时就可以给消息设定存活时间。RabbitMQ就有这种功能特性,它可以设置消息的存活时间(Time To Live,简称TTL),当消息到达存活时间后还没有被消费,会被移出队列。

有两种设置模式:1、对队列的所有消息设置存活时间(相当于给队列设置了有效时间);2、对某条消息设置存活时间。

6.3.1 对队列的所有消息设置存活时间

1、新增一个绑定队列和交换机的配置 RabbitSecondConfig.java 并给队列里的消息设置存活时间,设置的方法和代码块见 getSecondMessageQueue() 方法。

@Configuration
public class RabbitSecondConfig {
    // 指定交换机名称
    private final String EXCHANGE_NAME = "live_exchange";

    // 指定队列名
    private final String QUEUE_NAME = "live_queue";

    // 创建交换机
    @Bean("bootSecondExchange")
    public Exchange getSecondExchange() {
         return ExchangeBuilder
                 .topicExchange(EXCHANGE_NAME)
                 .durable(true) // 是否持久化
                 .build();
    }

    // 创建队列
    @Bean("bootSecondQueue")
    public Queue getSecondMessageQueue() {
        return QueueBuilder
                .durable(QUEUE_NAME)
                .ttl(15000) // 设定该队列里所有消息的存活时间是 15秒
                .build();
    }

    // 创建交换机绑定队列
    @Bean
    public Binding bindingSecondExchangeQueue(@Qualifier("bootSecondExchange") Exchange exchange, @Qualifier("bootSecondQueue") Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.happyNewYears.#")   // 通配符模式 要匹配的路由键 RoutingKey
                .noargs();
    }
}

2、新增生产者创建消息的方法,修改生产者 interdictProvider.java

@RestController
@RequestMapping("provider")
public class interdictProvider {
    // 注入 RabbitTemplate 工具类
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendNewYearMessage")
    public String sendMessage() {
        ...
    }

    /**
     * 生产者设定消息存活时间
     * @return
     */
    @GetMapping("/sendNewYearMessageOfTtl")
    public String sendSecondMessage() {
        int m=0;
        // 生产20条消息
        for (int i = 1; i <= 20; i++) {
            String messageId = String.valueOf(UUID.randomUUID());   // 随机一个消息 ID
            String messageContent = "快过年了,提前祝你新年快乐。第 "+i+"封信";   // 消息主题内容
            String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            HashMap<String, Object> messageMap = new HashMap<>();
            messageMap.put("messageId", messageId);
            messageMap.put("messageContent", messageContent);
            messageMap.put("sendTime", sendTime);

            /*
             * 发送消息
             * 参数1:交换机名称
             * 参数2:路由 routeKey
             * 参数3:消息主题内容
             */
            rabbitTemplate.convertAndSend("live_exchange", "happyNewYears", messageMap);
            m++;
        }
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"封新年祝福信已发送</p>";
    }
}

3、重启启动器,在浏览器中输入地址并发起请求,注意观察 RabbitMQ的控制台
TTL
当在队列设置了所有的消息存活时间时,会有 TTL 的标识。

总结:通过Demo 工程的演示可以看到 消息产出 15秒后,没有被消费的消息会从队列中自动删除。但是,在有些需求下不是所有队列的消息都要设置存活时间,可能只需要对某些极个别的消息设置存活时间。这样,当前这个设置就不合适该场景需求了,那如何对某个消息设置存活时间呢?

6.3.2 对某条消息设置存活时间

对某条消息设置了有效时间,如若该消息已超过有效时间过期了,此消息并不会被马上移除,而只有当这条消息消费到队列的顶端时,才会被移除;为了方便理解可见下图
TTL
1、修改 interdictProvider.java 新增消息生成者 sendTtlMessage() 方法。

@RestController
@RequestMapping("provider")
public class interdictProvider {
    // 注入 RabbitTemplate 工具类
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendNewYearMessage")
    public String sendMessage() {
        ...
    }

    /**
     * 生产者设定消息存活时间
     * @return
     */
    @GetMapping("/sendNewYearMessageOfTtl")
    public String sendSecondMessage() {
        ...
    }

    /**
     * 生产者设定消息存活时间
     * @return
     */
    @GetMapping("/sendNewYearTtlMessage")
    public String sendTtlMessage() {
        int m=0;
        // 生产5条消息
        for (int i = 1; i <= 5; i++) {
            // 设置消息主题
            String messageId = String.valueOf(UUID.randomUUID());   // 随机一个消息 ID
            String messageContent = "快过年了,提前祝你新年快乐。第 "+i+"封信";   // 消息主题内容
            String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            HashMap<String, Object> messageMap = new HashMap<>();
            messageMap.put("messageId", messageId);
            messageMap.put("messageContent", messageContent);
            messageMap.put("sendTime", sendTime);
            messageMap.put("expiration",20);

            if(i == 4) {
                /*
                * 模拟生成一条具有有效时间为1秒的消息
                * */
                // 设置消息属性
                MessageProperties messageProperties = new MessageProperties();
                // 设置消息存活时间
                messageProperties.setExpiration("1000");
                // 创建消息对象
                Message message = new Message(messageContent.getBytes(StandardCharsets.UTF_8), messageProperties);
                // 发送消息
                rabbitTemplate.convertAndSend("interdict_exchange", "happyNewYear", message);
            } else {
                /*
                 * 发送消息
                 * 参数1:交换机名称
                 * 参数2:路由 routeKey
                 * 参数3:消息主题内容
                 */
                rabbitTemplate.convertAndSend("interdict_exchange", "happyNewYear", messageMap);
            }
            m++;
        }
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"封新年祝福信已发送</p>";
    }
}

2、重启启动器、调用生产地址生产消息后,再启动消费者 interdictConsumer 项目进行消费,观察RabbitMQ控制台和终端

总结:如果在队列和指定的消息都设置了有效存活时间,则以时间短的为准,谁的有效时间短谁的优先级就高。

6.4 消息优先级

1、新增一个绑定队列和交换机的配置 RabbitPriorityConfig.java

@Configuration
public class RabbitPriorityConfig {
    // 定义交换机
    private final String EXCHANGE_NAME = "priority_exchange";

    // 定义队列
    private final String QUEUE_NAME = "priority_queue";

    //创建交换机
    @Bean(value = "bootPriorityExchange")
    public Exchange getPriorityExchange() {
        return ExchangeBuilder
                .topicExchange(EXCHANGE_NAME)
                .durable(true)  // 持久化
                .build();
    }

    // 创建队列
    @Bean(value = "bootPriorityQueue")
    public Queue getPriorityQueue() {
        return QueueBuilder
                .durable(QUEUE_NAME)
                .maxPriority(10) // 设置优先级参数值
                .build();
    }

    // 创建交换机绑定队列
    @Bean
    public Binding bindingPriorityExchangeQueue(@Qualifier("bootPriorityExchange") Exchange exchange,@Qualifier("bootPriorityQueue") Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.cx_active.#")
                .noargs();
    }
}

2、新增生产者创建消息的方法,修改生产者 interdictProvider.java

@RestController
@RequestMapping("provider")
public class interdictProvider {
    // 注入 RabbitTemplate 工具类
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendNewYearMessage")
    public String sendMessage() {
        ...
    }

    /**
     * 生产者设定消息存活时间
     * @return
     */
    @GetMapping("/sendNewYearMessageOfTtl")
    public String sendSecondMessage() {
        ...
    }

    /**
     * 生产者设定消息存活时间
     * @return
     */
    @GetMapping("/sendNewYearTtlMessage")
    public String sendTtlMessage() {
        ...
    }

    /**
     * 消息优先级
     * @return
     */
    @GetMapping("/sendCxActiveMessage")
    public String sendPriorityMessage() {
        // 数据模拟 定义10、12为vip 用户
        Map<Integer, String> vips = new HashMap<>();
        vips.put(10, "张三");
        vips.put(12, "李四");
        // 生产消息
        MessageProperties messageProperties = new MessageProperties();
        String messageContent = "";
        int m=0;
        for (int i = 1; i <= 50; i++) {
            String userName = vips.get(i);
            if(userName == null){
                messageContent = "限时活动,进入个人中心领红包了,先到先得,领完为止。";
            } else {
                messageContent = "尊贵的 VIP用户"+userName+ "您好,即可登录APP 进入个人中心领取全场 1000的通用红包,先到先得,领完为止。";
                messageProperties.setPriority(10);
            }
            Message message = new Message(messageContent.getBytes(StandardCharsets.UTF_8), messageProperties);
            rabbitTemplate.convertAndSend("priority_exchange","cxActive", message);
            m++;
        }
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"条消息已发送</p>";
    }
}

3、新增消费者

@Component
public class InterdictPriorityConsumer {
    @RabbitListener(queues = "priority_queue")
    public void listenMessage(Message message, Channel channel) throws IOException {
        System.out.println(new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
    }
}

由此,可见消息优先级越高越优先被消费

注意:优先级设置的过多,会消耗更多的CPU资源,因此,推荐优先级的值不超过10。

7.死信队列

死信队列概念:
在MQ中,当消息成为死信(Dead message)后,消息中间件可以将其从当前队列发送到另一个队列中,这个队列就是死信队列相当于电脑中垃圾回收站。而在RabbitMQ中,由于有交换机的概念,实际是将死信发送给了死信交换机(Dead Letter Exchange,简称DLX)。死信交换机和死信队列和普通的没有区别
死信流程
消息成为死信队列的情况:
1、队列消息长度到达限制。
2、消息到达存活时间未被消费。
3、消费者拒签消息,并且不把消息重新放入原队列。

延迟队列概念:
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。【经典案例:用户下单,30分钟后。订单未支付状态则会自动取消。】
订单
死信队列

SpringBoot 整合RabbitMQ的 pom.xml 文件依赖一致

构建部署生产者 deadProvider 项目
①、添加配置文件 application.yml

server:
  # 项目访问的端口号
  port: 9007
spring:
  application:
    # 项目名称
    name: dead-provider
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /

②、创建配置文件 DeadRabbitConfig.java 编写交换机、队列的绑定加入到 Spring 容器中

@Configuration
public class DeadRabbitConfig {
    // 定义死信交换机
    private final String DEAD_EXCHANGE = "dead_exchange";
    // 定义死信队列
    private final String DEAD_QUEUE = "dead_queue";

    // 普通交换机
    private final String ORDINARY_EXCHANGE = "ordinary_exchange";
    // 普通死信队列
    private final String ORDINARY_QUEUE = "ordinary_queue";

    // 死信交换机
    @Bean(DEAD_EXCHANGE)
    public Exchange deadExchange() {
        return ExchangeBuilder
                .topicExchange(DEAD_EXCHANGE)
                .durable(true) // 持久化
                .build();
    }

    // 死信队列
    @Bean(DEAD_QUEUE)
    public Queue deadQueue() {
        return QueueBuilder
                .durable(DEAD_QUEUE)
                .build();
    }

    // 死信交换机绑定死信队列
    @Bean
    public Binding bindDeadQueue(@Qualifier(DEAD_EXCHANGE) Exchange exchange, @Qualifier(DEAD_QUEUE) Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.deadRouting.#")
                .noargs();
    }

    // 普通交换机
    @Bean(ORDINARY_EXCHANGE)
    public Exchange ordinaryExchange() {
        return ExchangeBuilder
                .topicExchange(ORDINARY_EXCHANGE)
                .durable(true)  // 持久化
                .build();
    }

    // 普通队列
    @Bean(ORDINARY_QUEUE)
    public Queue ordinaryQueue() {
        return QueueBuilder
                .durable(ORDINARY_QUEUE)
                .deadLetterExchange(DEAD_EXCHANGE) // 绑定死信交换机
                .deadLetterRoutingKey("deadRouting") // 死信队列路由关键字
                .ttl(15000) // 消息存活时间 15秒
                .maxLength(10)  // 队列最大长度
                .build();
    }

    // 普通交换机绑定普通队列
    @Bean
    public Binding bindOrdinaryQueue(@Qualifier(ORDINARY_EXCHANGE) Exchange exchange, @Qualifier(ORDINARY_QUEUE) Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.happyNewYear.#")
                .noargs();
    }

}

③、创建生产者 DeadProvider.java

@RestController
@RequestMapping("provider")
public class DeadProvider {
    // 注入 RabbitTemplate 工具类
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 模拟演示当消息长度达到限制后,剩余消息进入死信
    @GetMapping("productMaxLengthMessage")
    public String productMaxLengthMessage() {
        return null;
    }

    // 模拟演示当消息过期后进行死信
    @GetMapping("productTtlMessage")
    public String productTtlMessage() {
        return null;
    }

    // 模拟演示消费者拒签后,消息进入死信
    @GetMapping("productRefuseMessage")
    public String productRefuseMessage() {
        return null;
    }
    
    protected String productMessage(int num) {
        int m = 0;
        for (int i = 1; i <= num; i++) {
            String messageId = String.valueOf(UUID.randomUUID());   // 随机一个消息 ID
            String messageContent = "快过年了,提前祝你新年快乐。第 "+i+"封信";   // 消息主题内容
            String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            HashMap<String, Object> messageMap = new HashMap<>();
            messageMap.put("messageId", messageId);
            messageMap.put("messageContent", messageContent);
            messageMap.put("sendTime", sendTime);

            /*
             * 发送消息
             * 参数1:交换机名称
             * 参数2:路由 routeKey
             * 参数3:消息主题内容
             */
            rabbitTemplate.convertAndSend("ordinary_exchange", "happyNewYear", messageMap);
            m++;
        }
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"封新年祝福信已发送</p>";
    }
}

构建部署生产者 deadConsumer 项目

spring:
  application:
    # 项目名称
    name: dead-consumer
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /

②、创建消费者 DeadConsumer.java

@Component
public class DeadConsumer {
    @RabbitListener(queues = "ordinary_queue")
    
    public void listenMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        
    }
}

7.1 死信一:队列消息长度到达限制

模拟演示当消息长度达到限制后,剩余消息进入死信。在配置文件 DeadRabbitConfig.java 给普通交换机ordinary_exchange 设置了交换机 最多只能产出10条消息,超过10条后的消息将进入死信。修改生产者 DeadProvider.java生产消息的方案 productMaxLengthMessage()

@GetMapping("productMaxLengthMessage")
public String productMaxLengthMessage() {
    return this.productMessage(100);
}

可以看出 普通队列只有10条消息,剩余超过10条的90条全部进入了死信队列。

7.2 死信二:消息到达存活时间未被消费

修改生产者 DeadProvider.java生产消息的方案 productTtlMessage()

// 模拟演示当消息过期后进行死信
@GetMapping("productTtlMessage")
public String productTtlMessage() {
    return this.productMessage(10);
}

重新启动生产者启动器,删除已存在的队列并在浏览器中访问

可以看到消息超过有效15秒的时间后,消息进入死信。

7.3 死信三:消费者拒签消息,并且不把消息重新放入原队列

修改 消费者 DeadConsumer.java

@Component
public class DeadConsumer {
    @RabbitListener(queues = "ordinary_queue")

    public void listenMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        channel.basicNack(deliveryTag, true, false); // 消息拒签
    }
}

启动消费者 并在浏览器中运行

可以看到,消费者拒签以后,消息立马进入死信队列。

7.4 基于死信队列的延迟队列

SpringBoot订单生产者(orderProvider)

①、SpringBoot 整合RabbitMQ的 pom.xml 文件依赖一致如前言部分创建死信队列demo 时相同

②、添加配置文件 application.yml

server:
  # 项目访问的端口号
  port: 9010
spring:
  application:
    # 项目名称
    name: order-provider
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /

③、创建配置文件 OrderRabbitConfig.java 编写交换机、队列的绑定加入到 Spring 容器中

@Configuration
public class OrderRabbitConfig {
    // 定义普通正常的交换机
    private final String ORDER_EXCHANGE = "order_exchange";
    // 定义普通正常的队列
    private final String ORDER_QUEUE = "order_queue";

    // 定义死信过期过期的交换机
    private final String EXPIRE_EXCHANGE = "expire_exchange";
    // 定义死信过期过期的队列
    private final String EXPIRE_QUEUE = "expire_queue";

    // 死信过期交换机
    @Bean(EXPIRE_EXCHANGE)
    public Exchange expireExchange() {
        return ExchangeBuilder
                .topicExchange(EXPIRE_EXCHANGE)
                .durable(true)
                .build();
    }

    // 死信过期队列
    @Bean(EXPIRE_QUEUE)
    public Queue expireQueue() {
        return QueueBuilder
                .durable(EXPIRE_QUEUE)
                .build();
    }

    // 死信过期交换机和队列进行绑定
    @Bean
    public Binding bindExpireExchangeQueue(@Qualifier(EXPIRE_EXCHANGE) Exchange exchange,@Qualifier(EXPIRE_QUEUE) Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.expire_orderRouting.#")
                .noargs();
    }

    // 正常订单交换机
    @Bean(ORDER_EXCHANGE)
    public Exchange orderExchange() {
        return ExchangeBuilder
                .topicExchange(ORDER_EXCHANGE)
                .durable(true)
                .build();
    }
    
    // 正常订单队列
    @Bean(ORDER_QUEUE)
    public Queue orderQueue() {
        return QueueBuilder
                .durable(ORDER_QUEUE)
                .ttl(15000) // 正常队列消息存活时间模拟订单 30分钟为 10秒
                .deadLetterExchange(EXPIRE_EXCHANGE)    // 绑定死信过期交换机
                .deadLetterRoutingKey("expire_orderRouting")    // 绑定死信过期交换机的路由关键字
                .build();
                
    }
    
    // 正常订单交换机和队列绑定
    @Bean
    public Binding bindExchangeQueue(@Qualifier(ORDER_EXCHANGE) Exchange exchange,@Qualifier(ORDER_QUEUE) Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.orderRouting.#")
                .noargs();
    }
}

④、创建生产者 OrderProviderController.java

@RestController
@RequestMapping("provider")
public class OrderProviderController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/makeOrder")
    public String makeOrder() {
        String orderId = String.valueOf(UUID.randomUUID()); // 生成随机订单号
        String orderInfo = "迈巴赫 S480";
        HashMap<String, String> orderMap = new HashMap<>();
        orderMap.put("orderId", orderId);
        orderMap.put("orderInfo", orderInfo);
        rabbitTemplate.convertAndSend("order_exchange","orderRouting",orderMap);
        System.out.println("付款中...");
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>下单成功,订单号为:"+orderId+"</p>";
    }
}

⑤、创建消费者 OrderConsumerController.java监听消息

@Component
public class OrderConsumerController {
    // 监听队列
    @RabbitListener(queues = "expire_queue")
    public void listenOrder(Message message) throws Exception {
        byte[] body = message.getBody();
        ObjectInputStream orderStream = new ObjectInputStream(new ByteArrayInputStream(body));
        Map<String, String> orderObj = (Map<String, String>) orderStream.readObject();
        String orderId = orderObj.get("orderId");
        String orderInfo = orderObj.get("orderInfo");
        System.out.println("付款成功,喜提一辆 "+orderInfo+",订单号为:"+orderId);
    }
}

⑥、启动生产者,观察RabbitMQ 控制台和终端

基于死信队列的延迟队列的使用看着使用也是很简单,但是一个消息要进入到死信队列,必须得满足当前这个消息消费到队列的顶端时才会进入到死信队列。所以,在使用死信队列实现延迟队列时,定会遇到这样的问题:RabbitMQ只会移除队列顶端的过期消息,如果第一个消息的存活时长较长,而第二个消息的存活时长较短,则第二个消息并不会及时执行,这样就很影响业务的正常运行。RabbitMQ 虽本身不能使用延迟队列,但是为了解决这个问题,官方提供了延迟队列插件,安装后可直接使用延迟队列。
延迟队列

7.5 延迟队列的安装

Docker 容器安装方法

1、将文件 移动到容器里

docker cp 你的宿主机rabbitmq_delayed_message_exchange-3.9.0.ez文件路径 容器长ID:docker容器路径
例如:
docker cp rabbitmq_delayed_message_exchange-3.9.0.ez 095a83361bf9:/plugins

2、进入容器

docker exec -it 你的RabbitMQ名称或容器id /bin/bash

3、启动插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

4、查看插件集合

rabbitmq-plugins list

5、退出重启容器

docker restart 你的RabbitMQ名称或容器id

最后出现 RabbitMQ 控制台 出现 x-delayed-message 说明插件安装成功
管控台
基于延迟队列的订单Demo

SpringBoot订单生产者(orderDelayedProvider项目)基于 orderProvider项目进行改写

①、添加配置文件 application.yml

server:
  # 项目访问的端口号
  port: 9011
spring:
  application:
    # 项目名称
    name: order-delayedProvider
  # 配置RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: logic
    password: 123456
    # 虚拟主机
    virtual-host: /

②、交换机与队列进行绑定 创建 OrderDelayedRabbitConfig.java 注入到 Spring容器中

@Configuration
public class OrderDelayedRabbitConfig {
    // 定义延迟交换机
    private final String DELAYED_EXCHANGE = "delayed_exchange";
    // 定义延迟的队列
    private final String DELAYED_QUEUE = "delayed_queue";


    // 延迟交换机
    @Bean(DELAYED_EXCHANGE)
    public Exchange expireExchange() {
        // 修正创建自定义的延迟交换机
        HashMap<String, Object> exchange = new HashMap<>();
        exchange.put("x-delayed-type","topic"); // topic 类型的延迟交换机
        /**
         * 参数1:交换机名称
         * 参数2:类型必须是 x-delayed-message
         * 参数3:是否持久化(队列的声明默认是存放到内存中的,如果rabbitmq重启会丢失,如果想重启之后还存在就要使队列持久化)
         * 参数4:是否自动删除(队列中的数据消费完成后是否自动删除队列,当最后一个消费者断开连接之后队列是否自动被删除)
         * 参数5:自定义交换机的 HashMap
         */
        return new CustomExchange(DELAYED_EXCHANGE,"x-delayed-message",true,false,exchange);
    }
    
    // 延迟队列
    @Bean(DELAYED_QUEUE)
    public Queue expireQueue() {
        return QueueBuilder
                .durable(DELAYED_QUEUE)
                .build();
    }

    // 延迟交换机和队列进行绑定
    @Bean
    public Binding bindDelayedExchangeQueue(@Qualifier(DELAYED_EXCHANGE) Exchange exchange,@Qualifier(DELAYED_QUEUE) Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("#.delayed_orderRouting.#")
                .noargs();
    }
}

③、编写生产者 OrderDelayedProviderController.java

@RestController
@RequestMapping("provider")
public class OrderDelayedProviderController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/makeOrder")
    public String makeOrder() {
        // 设置消息的有效时间为15秒 模拟订单有效的支付时间
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {

            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setDelay(15000);
                return message;
            }
        };

        // 创建订单信息
        String orderId = String.valueOf(UUID.randomUUID()); // 生成随机订单号
        String orderInfo = "迈巴赫 S480";
        HashMap<String, String> orderMap = new HashMap<>();
        orderMap.put("orderId", orderId);
        orderMap.put("orderInfo", orderInfo);
        // 将订单信息发送到 队列
        rabbitTemplate.convertAndSend("delayed_exchange","delayed_orderRouting",orderMap,messagePostProcessor);
        System.out.println("付款中...");
        return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>下单成功,订单号为:"+orderId+"</p>";
    }
}

④、监听消费者 OrderDelayedConsumerController.java

@Component
public class OrderDelayedConsumerController {
    // 监听队列
    @RabbitListener(queues = "delayed_queue")
    public void listenOrder(Message message) throws Exception {
        byte[] body = message.getBody();
        ObjectInputStream orderStream = new ObjectInputStream(new ByteArrayInputStream(body));
        Map<String, String> orderObj = (Map<String, String>) orderStream.readObject();
        String orderId = orderObj.get("orderId");
        String orderInfo = orderObj.get("orderInfo");
        System.out.println("付款成功,喜提一辆 "+orderInfo+",订单号为:"+orderId);
    }
}

⑥、启动生产者,观察RabbitMQ 控制台和终端

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值