RabbitMQ简介
消息中间件
概述
消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串、 JSON 等,也可以很复杂,比如内嵌对象。
消息队列中间件(Message Queue Middleware,简称为 MQ)是指利用高效可靠的消息传递 机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传 递和消息排队模型,它可以在分布式环境下扩展进程间的通信。

消息队列中间件,也可以称为消息队列或者消息中间件。它一般有两种传递模式:点对点 (P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。
点对点模式是基于队列的,消息生产 者发送消息到队列,消息消费者从队列中接收消息,队列的存在使得消息的异步传输成为可能。发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(topic),主 题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题中 订阅消息。主题使得消息的订阅者与消息的发布者互相保持独立,不需要进行接触即可保证消 息的传递,发布/订阅模式在消息的一对多广播时采用。
目前开源的消息中间件有很多,比较主流的有 RabbitMQ、Kafka、ActiveMQ、RocketMQ 等。面向消息的中间件(简称为 MOM,Message Oriented Middleware)提供了以松散耦合的灵 活方式集成应用程序的一种机制。它们提供了基于存储和转发的应用程序之间的异步数据发送, 即应用程序彼此不直接通信,而是与作为中介的消息中间件通信。消息中间件提供了有保证的 消息发送,应用程序开发人员无须了解远程过程调用(RPC)和网络通信协议的细节。
如下图,应用程序 A 与应用程序 B 通过使用消息中间件的应用程序编程接口(API,Application Program Interface)发送消息来进行通信。

消息中间件将消息路由给应用程序 B,这样消息就可存在于完全不同的计算机上。消息中 间件负责处理网络通信,如果网络连接不可用,消息中间件会存储消息,直到连接变得可用, 再将消息转发给应用程序 B。
灵活性的另一方面体现在,当应用程序 A 发送其消息时,应用程 序 B 甚至可以处于不运行状态,消息中间件将保留这份消息,直到应用程序 B 开始执行并消费 消息,这样还防止了应用程序 A 因为等待应用程序 B 消费消息而出现阻塞。这种异步通信方式 要求应用程序的设计与现在大多数应用不同。不过对于时间无关或并行处理的场景,它可能是 一个极其有用的方法。
作用
解耦:在项目启动之初来预测将来会碰到什么需求是极其困难的。消息中间件在处理过程 中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,这允许你独 立地扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束即可。
冗余(存储):有些情况下,处理数据的过程会失败。消息中间件可以把数据进行持久化直 到它们已经被完全处理,通过这一方式规避了数据丢失风险。
扩展性:因为消息中间件解耦了应用的处理过程,所以提高消息入队和处理的效率是很容 易的,只要另外增加处理过程即可,不需要改变代码,也不需要调节参数。
削峰:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常 见。如果以能处理这类峰值为标准而投入资源,无疑是巨大的浪费。使用消息中间件能够使关 键组件支撑突发访问压力,不会因为突发的超负荷请求而完全崩溃。
可恢复性:当系统一部分组件失效时,不会影响到整个系统。消息中间件降低了进程间的 耦合度,所以即使一个处理消息的进程挂掉,加入消息中间件中的消息仍然可以在系统恢复后 进行处理。
顺序保证:在大多数使用场景下,数据处理的顺序很重要,大部分消息中间件支持一定程 度上的顺序性。
缓冲:在任何重要的系统中,都会存在需要不同处理时间的元素。消息中间件通过一个缓 冲层来帮助任务最高效率地执行,写入消息中间件的处理会尽可能快速。该缓冲层有助于控制 和优化数据流经过系统的速度。
异步通信:在很多时候应用不想也不需要立即处理消息。消息中间件提供了异步处理机制, 允许应用把一些消息放入消息中间件中,但并不立即处理它,在之后需要的时候再慢慢处理。
为何要用消息队列
消息队列 CMQ )是一种系统间相互协作的通信机制 。那么 什么时候需要使用消息队列呢 ?
举个例子 。 某天产 品人员说“系统要增加一个用户注册功能,注册成功后用户能收到邮件 通知”。在实际场景中这种需求很常见,开发人员觉得这个很简单,就是提供一个注册页面,点 击按钮提交之后保存用户 的注册信息 , 然后发送邮件,最后返回用 户注册成功。

该功能上线运行了一段时间后,产品人员说“点击注册按钮之后 响应有点慢,很多人都提 出这个意见,能不能优化一下”。 开发人员首先想到的优化方案是将保存注册信息与发送邮件分 开执行,怎么分呢?可以单独启动线程来做发送邮件的事情。

没多久, 产 品人员又说“现在注册操作的响应是快了,但有用户反映没收到注册成功的邮件,能不能在发送邮件的时候先保存所发送邮件的内容,如果邮件发送失败了 则进行补发”。
看着开发人员愁眉苦脸的样子,产品人员说“在邮件发送这块平台部 门 已经做好方案了 ,你直接用他们提供的服务就行”。开发人员 一昕, 赶紧和平台部门沟通,对方的答复是“我们提 供一个类似于阳局信箱的东西,你直接往这个信箱里写上发送 邮件的地址、邮件标题和内容, 之后就不用 你操心 了,我们会直接从信箱里取信息,向你所填写的邮件地址发送响应邮件”。
这个故事讲的就是使用消息队列的典型场景一一异步处理。 流量削峰、日志收集、事务最终一致性等问题。
AMQP

Publisher:消息发送者,将消息发送到Exchange并指定RoutingKey,以便queue可以接收到指定的消息。
Consumer:消息消费者,从queue获取消息,一个Consumer可以订阅多个queue以从多个queue中接收消息。
Server:一个具体的MQ服务实例,也称为Broker。
Virtual host:虚拟主机,一个Server下可以有多个虚拟主机,用于隔离不同项目,一个Virtual host通常包含多个Exchange、MessageQueue。
Exchange:交换器,接收Producer发送来的消息,把消息转发到对应的Message Queue中。
Routing key:路由键,用于指定消息路由规则(Exchange将消息路由到具体的queue中),通 常需要和具体的Exchange类型、Binding的Routing key结合起来使用。
Bindings:指定了Exchange和Queue之间的绑定关系。Exchange根据消息的Routing key和 Binding配置(绑定关系、Binding、Routing key等)来决定把消息分派到哪些具体的queue中。这依 赖于Exchange类型。
Message Queue:实际存储消息的容器,并把消息传递给最终的Consumer。
RabbitMQ概念
RabbitMQ 是采用 Erlang 语言实现 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件。
AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP。
2007年,Rabbit 技术公司基于 AMQP 标准开发的 RabbitMQ 1.0 发布。RabbitMQ 采用 Erlang 语言开发。Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。
RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
核心概念
1.生产者
产生数据发送消息的程序是生产者。
2.交换机
交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定。
3.队列
队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式。
4.消费者
消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。
工作原理

- Broker:接收和分发消息的应用,
RabbitMQ Server就是 Message Broker。 - Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建
exchange/queue等。 - Connection:
publisher/consumer和broker之间的 TCP 连接。 - Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在
connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销 。 - Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的
routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point),topic (publish-subscribe)andfanout (multicast)。 - Queue:消息最终被送到这里等待 consumer 取走。
- Binding:
exchange和queue之间的虚拟连接,binding 中可以包含routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据。
RabbitMQ安装
前面提到了 RabbitMQ 是由 Erlang 语言编写的,也正因如此,在安装 RabbitMQ 之前需要 安装 Erlang。
安装Erlang
Erlang官网:www.erlang.org/
Linux下载:github.com/erlang/otp/…
- 安装erlang前先安装Linux依赖库
yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel
- 下载tar包
wget https://github.com/erlang/otp/releases/download/OTP-26.1.2/otp_src_26.1.2.tar.gz
- 解压erlang压缩包文件
tar -zxvf otp_src_25.1.1.tar.gz
- 配置
切换到解压的目录下,运行相应命令
cd otp_src_25.1.1
./configure
- 编译 &安装
make && make install
- 验证erlang是否安装成功
在命令行输入: erl 如果进入了编程命令行则表示安装成功,然后按ctrl + z 退出编程命令行;
erl
安装RabbitMQ
- linux下载tar包
wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.12.7/rabbitmq-server-generic-unix-3.12.7.tar.xz
- 解压RabbitMQ的压缩包
tar -xvf rabbitmq-server-generic-unix-3.12.7.tar.xz
- 修改/etc/profile 文件,添加下面的环境变量:
export PATH=$PATH:/opt/rabbitmq_server-3.12.7/sbin
export RABBITMQ_HOME=/opt/rabbitmq_server-3.12.7
-
之后执行
source/etc/profile命令让配置文件生效。 -
输入如下命令以 运行 RabbitMQ 服务:
rabbitmq-server –detached
在 rabbitmq-server 命令后面添加一个“-detached”参数是为了能够让 RabbitMQ 服务以守护进程的方式在后台运行,这样就不会因为当前 Shell 窗口的关闭而影响服务。
- 运行 rabbitmqctl status 命令查看 RabbitMQ 是否正常启动,示例如下:
rabbitmqctl status
可以看到在安装命令中有两个映射的端口:
- 15672:RabbitMQ提供的管理控制台的端口
- 5672:RabbitMQ的消息发送处理接口
web后台登录
rabbitmqctl 是一个管理命令,可以管理rabbitmq的很多操作。
rabbitmqctl help #可以查看一下有哪些操作
用户管理包括增加用户,删除用户,查看用户列表,修改用户密码。
这些操作都是通过rabbitmqctl管理命令来实现完成。
1.添加用户
rabbitmqctl add_user root 123456
2.给用户添加权限
给root用户在虚拟主机"/"上的配置、写、读的权限
rabbitmqctl set_permissions root -p / ".*" ".*" ".*"
3.给用户设置标签
rabbitmqctl set_user_tags root administrator
Rabbitmq有一个web管理后台,这个管理后台是以插件的方式提供的,启动后台web管理功能:
# rabbitmq-plugins enable rabbitmq_management
4.访问web页面
用户名/密码为我们上面创建的root/123456
备注:如果使用默认用户guest、密码guest登录,会提示User can only log in via localhost
说明guest用户只能从localhost本机登录,所以不要使用该用户。

常用操作命令
# 前台启动Erlang VM和RabbitMQ
rabbitmq-server
# 后台启动
rabbitmq-server -detached
# 停止RabbitMQ和Erlang VM
rabbitmqctl stop
# 查看所有队列
rabbitmqctl list_queues
# 查看所有虚拟主机
rabbitmqctl list_vhosts
# 在Erlang VM运行的情况下启动RabbitMQ应用
rabbitmqctl start_app
rabbitmqctl stop_app
# 查看节点状态
rabbitmqctl status
# 查看所有可用的插件
rabbitmq-plugins list
# 启用插件
rabbitmq-plugins enable <plugin-name>
# 停用插件
rabbitmq-plugins disable <plugin-name>
# 添加用户
rabbitmqctl add_user username password
# 列出所有用户:
rabbitmqctl list_users
# 删除用户:
rabbitmqctl delete_user username
# 清除用户权限:
rabbitmqctl clear_permissions -p vhostpath username
# 列出用户权限:
rabbitmqctl list_user_permissions username
# 修改密码:
rabbitmqctl change_password username newpassword
# 设置用户权限:
rabbitmqctl set_permissions -p vhostpath username ".*" ".*" ".*"
# 创建虚拟主机:
rabbitmqctl add_vhost vhostpath
# 列出所以虚拟主机:
rabbitmqctl list_vhosts
# 列出虚拟主机上的所有权限:
rabbitmqctl list_permissions -p vhostpath
# 删除虚拟主机:
rabbitmqctl delete_vhost vhost vhostpath
# 移除所有数据,要在
rabbitmqctl stop_app 之后使用: rabbitmqctl reset
RabbitMQ对应的架构如图:

其中包含几个概念:
publisher:生产者,也就是发送消息的一方consumer:消费者,也就是消费消息的一方queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
上述这些东西都可以在RabbitMQ的管理控制台来管理,下一节我们就一起来学习控制台的使用。
收发消息
交换机
我们打开Exchanges选项卡,可以看到已经存在很多交换机:

我们点击任意交换机,即可进入交换机详情页面。仍然会利用控制台中的publish message 发送一条消息:


这里是由控制台模拟了生产者发送的消息。由于没有消费者存在,最终消息丢失了,这样说明交换机没有存储消息的能力。
队列
我们打开Queues选项卡,新建一个队列:

命名为hello.queue1:

再以相同的方式,创建一个队列,密码为hello.queue2,最终队列列表如下:

此时,我们再次向amq.fanout交换机发送一条消息。会发现消息依然没有到达队列!!
怎么回事呢?
发送到交换机的消息,只会路由到与其绑定的队列,因此仅仅创建队列是不够的,我们还需要将其与交换机绑定。
绑定关系
点击Exchanges选项卡,点击amq.fanout交换机,进入交换机详情页,然后点击Bindings菜单,在表单中填写要绑定的队列名称:

相同的方式,将hello.queue2也绑定到改交换机。
最终,绑定结果如下:

发送消息
再次回到exchange页面,找到刚刚绑定的amq.fanout,点击进入详情页,再次发送一条消息:

回到Queues页面,可以发现hello.queue中已经有一条消息了:

点击队列名称,进入详情页,查看队列详情,这次我们点击get message:

可以看到消息到达队列了:

这个时候如果有消费者监听了MQ的hello.queue1或hello.queue2队列,自然就能接收到消息了。
生产和消费消息
本节将演示如何使用 RabbitMQ Java 客户端生产和消费消息。首先生产者发送一条消 息“Hello World!”至 RabbitMQ 中,之后由消费者消费。
在下图中,“P”是我们的生产者,“C”是我们的消费者。中间的框是一个队列 - RabbitMQ 代表消费者保留的消息缓冲区。

生产者
public class RabbitProducer {
//队列名称
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws Exception {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂IP 连接RabbitMQ的队列
factory.setHost("116.205.231.12");
//设置用户名
factory.setUsername("root");
//设置密码
factory.setPassword("123456");
//创建连接
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化,默认消息存在内存中
* 3.该队列是否只供一个消费者进行消费,是否进行消息共享,true可以多个消费者消费,false只能一个消费者消费
* 4.是否自动删除,最后一个消费者断开连接以后,该队列是否自动删除,true自动删除,false不自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//发消息
String message="hello world";//初次使用
/**
* 发送一个消费
* 1.发送到哪个交换机,如果没有指定则使用默认Default Exchage
* 2.路由到key值是哪个,本次是队列的名称
* 3.其他参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("消息发送完毕");
// 关闭资源
channel.close();
connection.close();
}
}
消费者
public class RabbitConsumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("116.205.231.12");
factory.setUsername("root");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println("等待接收消息....");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
System.out.println(message);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println("消息消费被中断");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*4.消费者取消消费的回调
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
测试结果:
生产者

消费者

SpringAMQP
- 在pom文件中引入对应的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 在application.yml配置文件中配置RabbitMQ
spring:
rabbitmq:
host: 116.205.231.12 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: root # 用户名
password: 123456 # 密码
- 配置文件中声明队列
@SpringBootConfiguration
public class RabbitMqConfig {
/**
* hello队列名称
*/
public static final String HELLO_MSG_QUEUE = "hello";
/**
* 声明hello队列
*
* @return
*/
@Bean
public Queue getHelloQueue() {
//参数一:队列名;
//参数二:durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
return new Queue(HELLO_MSG_QUEUE, true);
}
}
- 创建生产者
@RestController
public class Tut1Sender {
@Autowired
private RabbitTemplate template;
@Autowired
private Queue queue;
@GetMapping("/sendMessage")
public void send() {
String message = "Hello World!";
// 将消息携带绑定键值:message,发送到交换机:hello
//参数:
//(1)routingKey为要发送的路由地址。
//(2)message为具体的消息内容。发送者和接收者的queuename必须一致,不然无法接收。
this.template.convertAndSend(queue.getName(), message);
System.out.println("发送的消息是" + message);
}
}
消息发送成功后,在web管理页面查看:

可以看到对应队列中产生了消息。
- 创建消费者
@RabbitListener(queues = "hello")
@Component
public class Tut1Receiver {
@RabbitHandler
public void receive(String in) {
System.out.println("收到的消息是" + in);
}
}
- 测试:

可以看到Java客户端代码非常复杂,所以后面我们都会使用Spring进行操作。
注解
@RabbitListener
@RabbitListener用于在Spring Boot应用程序中创建消费者端接收和处理消息的方法。它是基于Spring和RabbitMQ实现的,可以用于消费者端消费RabbitMQ队列中的消息。
@RabbitListener的作用是:
-
声明该方法是一个RabbitMQ消息监听器,用于接收指定队列中的消息。
-
自动创建和配置一个RabbitMQ连接工厂,并绑定到指定的队列。
-
自动创建和配置一个RabbitMQ消费者,并在消息到达时调用带有
@RabbitHandler注解的方法处理消息。 -
允许通过一些配置参数来定制RabbitMQ连接工厂和消费者的行为,例如设置消息的自动确认模式、设置并发消费者数、设置消息转换器等。
@RabbitListener(queues = "myQueue")
public void receiveMessage(String message) {
System.out.println("Received message: " + message);
}
上述方法用于监听名为“myQueue”的RabbitMQ队列,一旦有消息到达该队列,就会自动调用该方法,并将消息的内容作为参数传递给该方法。在本例中,该方法会简单地将消息的内容打印到控制台上。
类和方法
除了加在类名上之外,还可以加在方法上,那么@RabbitListener注解加在类上和加在方法上有什么区别?
正如上面讲的,当@RabbitListener注解加在类上时,表示该类是一个RabbitMQ消息监听器容器,可以包含多个带有@RabbitHandler注解的方法,用于处理不同类型的消息。例如:
@RabbitListener(queues = "myQueue")
public class MyMessageListener {
@RabbitHandler
public void handleStringMessage(String message) {
// 处理字符串类型的消息
}
@RabbitHandler
public void handleObjectMessage(MyObject message) {
// 处理自定义对象类型的消息
}
}
上述代码定义了一个名为MyMessageListener的类,并使用@RabbitListener注解标记该类监听名为“myQueue”的RabbitMQ队列。类中定义了两个带有@RabbitHandler注解的方法,用于分别处理字符串类型和自定义对象类型的消息。
如果@RabbitListener加在类上面,需要有一个默认的处理方法
@RabbitHandler(isDefault=true),默认是false。
当@RabbitListener注解加在方法上时,表示该方法是一个RabbitMQ消息监听器,用于接收指定队列中的消息。例如:
public class MyMessageListener {
@RabbitListener(queues = "myQueue")
public void handleStringMessage(String message) {
// 处理字符串类型的消息
}
}
上述代码定义了一个名为handleStringMessage的方法,并使用@RabbitListener注解标记该方法监听名为“myQueue”的RabbitMQ队列。方法中定义了一个参数message,用于接收消息的内容。
@RabbitListener注解加在类上时,表示该类是一个消息监听器容器,可以包含多个处理不同类型消息的方法;而加在方法上时,表示该方法是一个消息监听器,只用于处理特定类型的消息。
ackMode:覆盖容器工厂 AcknowledgeMode属性。
admin:参考AmqpAdmin.
autoStartup:设置为 true 或 false,以覆盖容器工厂中的默认设置。
QueueBinding[] bindings:QueueBinding提供监听器队列名称以及交换和可选绑定信息的数组。
concurrency:消费并发数。
containerFactory:RabbitListenerContainerFactory的bean名称 ,没有则使用默认工厂。
converterWinsContentType:设置为“false”以使用“replyContentType”属性的值覆盖由消息转换器设置的任何内容类型标头。
errorHandler:消息异常时调用的方法名。
exclusive:当为true时,容器中的单个消费者将独占使用 queues(),从而阻止其他消费者从队列接收消息。
executor:线程池bean的名称
group:如果提供,则此侦听器的侦听器容器将添加到以该值作为其名称的类型为 的 bean 中Collection<MessageListenerContainer>。
id:为此端点管理的容器的唯一标识符。
messageConverter:消息转换器。
priority:此端点的优先级。
String[] queues:监听的队列名称
Queue[] queuesToDeclare:监听的队列Queue注解对象,与bindings()、queues()互斥。
replyContentType:用于设置回复消息的内容类型。
replyPostProcessor:在ReplyPostProcessor发送之前处理响应的 bean 名称 。
returnExceptions:设置为“true”以导致使用正常replyTo/@SendTo语义将侦听器抛出的异常发送给发送者。
Queue接口
name: 队列的名称;
durable: 是否持久化;
exclusive: 是否独享、排外的;
autoDelete: 是否自动删除;
arguments:队列的其他属性参数,有如下可选项,可参看图2的arguments:
x-message-ttl:消息的过期时间,单位:毫秒;
x-expires:队列过期时间,队列在多长时间未被访问将被删除,单位:毫秒;
x-max-length:队列最大长度,超过该最大值,则将从队列头部开始删除消息;
x-max-length-bytes:队列消息内容占用最大空间,受限于内存大小,超过该阈值则从队列头部开始删除消息;
x-overflow:设置队列溢出行为。这决定了当达到队列的最大长度时消息会发生什么。有效值是drop-head、reject-publish或reject-publish-dlx。仲裁队列类型仅支持drop-head;
x-dead-letter-exchange:死信交换器名称,过期或被删除(因队列长度超长或因空间超出阈值)的消息可指定发送到该交换器中;
x-dead-letter-routing-key:死信消息路由键,在消息发送到死信交换器时会使用该路由键,如果不设置,则使用消息的原来的路由键值
x-single-active-consumer:表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false)
x-max-priority:队列要支持的最大优先级数;如果未设置,队列将不支持消息优先级;
x-queue-mode(Lazy mode):将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息;
x-queue-master-locator:在集群模式下设置镜像队列的主节点信息。
RabbitMQ架构
相关概念介绍
RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消 息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收 件人的手上,RabbitMQ 就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说, RabbitMQ 模型更像是一种交换机模型。
RabbitMQ 的整体模型架构如图所示。

生产者和消费者
Producer:生产者,就是投递消息的一方。
生产者创建消息,然后发布到 RabbitMQ 中。消息一般可以包含 2 个部分:消息体和标签 (Label)。消息体也可以称之为 payload,在实际应用中,消息体一般是一个带有业务逻辑结构 的数据,比如一个 JSON 字符串。当然可以进一步对这个消息体进行序列化操作。消息的标签用来表述这条消息, 比如一个交换器的名称和一个路由键。 生产者把消息交由 RabbitMQ, RabbitMQ 之后会根据标签把消息发送给感兴趣的消费者(Consumer)。
Consumer:消费者,就是接收消息的一方。
消费者连接到 RabbitMQ 服务器,并订阅到队列上。当消费者消费一条消息时,只是消费 消息的消息体(payload)。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只 有消息体,消费者也只会消费到消息体,也就不知道消息的生产者是谁,当然消费者也不需要 知道。
Broker:消息中间件的服务节点。
对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点, 或者 RabbitMQ 服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。
下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从 Broker 中消费数据的整 个流程。

首先生产者将业务方数据进行可能的包装,之后封装成消息,发送(AMQP 协议里这个动 作对应的命令为 Basic.Publish)到 Broker 中。消费者订阅并接收消息(AMQP 协议里这个动作对应的命令为Basic.Consume或者Basic.Get),经过可能的解包处理得到原始的数据, 之后再进行业务处理逻辑。这个业务处理逻辑并不一定需要和接收消息的逻辑使用同一个线程。 消费者进程可以使用一个线程去接收消息,存入到内存中,比如使用 Java 中的 BlockingQueue。 业务处理逻辑使用另一个线程从内存中读取数据,这样可以将应用进一步解耦,提高整个应用 的处理效率。
队列
Queue:队列,是 RabbitMQ 的内部对象,用于存储消息。如下图所示:

RabbitMQ 中消息都只能存储在队列中,这一点和 Kafka 这种消息中间件相反。Kafka 将消 息存储在 topic(主题)这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移 标识。RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询) 给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,如图所示。

交换器、路由键、绑定
Exchange:交换器。在图中我们暂时可以理解成生产者将消息投递到队列中,实际上 这个在 RabbitMQ 中不会发生。真实情况是,生产者将消息发送到 Exchange(交换器,通常也 可以用大写的“X”来表示),由交换器将消息路由到一个或者多个队列中。如果路由不到,或 许会返回给生产者,或许直接丢弃。这里可以将 RabbitMQ 中的交换器看作一个简单的实体, 更多的细节会在后面的章节中有所涉及。
交换器的具体示意图如图所示。

RabbitMQ 中的交换器有四种类型,不同的类型有着不同的路由策略,这将在下一节的交换 器类型(Exchange Types)中介绍。
RoutingKey:路由键。生产者将消息发给交换器的时候,一般会指定一个 RoutingKey,用 来指定这个消息的路由规则,而这个 Routing Key 需要与交换器类型和绑定键(BindingKey)联 合使用才能最终生效。
在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以在发送消息给交换器时, 通过指定 RoutingKey 来决定消息流向哪里。
Binding:绑定。RabbitMQ 中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样 RabbitMQ 就知道如何正确地将消息路由到队列了,如图所示。

生产者将消息发送给交换器时,需要一个 RoutingKey,当 BindingKey 和 RoutingKey 相匹 配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许 使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比 如 fanout 类型的交换器就会无视 BindingKey,而是将消息路由到所有绑定到该交换器的队列中。
沿用本章开头的比喻,交换器相当于投递包裹的邮箱,RoutingKey 相当于填写在包裹上的 地址,BindingKey 相当于包裹的目的地,当填写在包裹上的地址和实际想要投递的地址相匹配 时,那么这个包裹就会被正确投递到目的地,最后这个目的地的“主人”——队列可以保留这 个包裹。如果填写的地址出错,邮递员不能正确投递到目的地,包裹可能会回退给寄件人,也 有可能被丢弃。
创建交换器
发布订阅使 用fanout。创建交换器,名字叫 logs :
channel.exchangeDeclare("logs", "fanout");
在前面的那里中我们没有指定交换器,但是依然可以向队列发送消息。这是因为我们使用了默认的交换器。
channel.basicPublish("", "hello", null, message.getBytes());
第一个参数就是交换器名称,为空字符串。直接使用routingKey向队列发送消息,如果该 routingKey指定的队列存在的话。
现在,向指定的交换器发布消息:
channel.basicPublish("logs", "", null, message.getBytes());
创建队列
前面我们使用队列的名称,生产者和消费者都是用该名称来发送和接收该队列中的消息。
首先,我们无论何时连接RabbitMQ的时候,都需要一个新的,空的队列。我们可以使用随机的名 字创建队列,也可以让服务器帮我们生成随机的消息队列名字。
其次,一旦我们断开到消费者的连接,该队列应该自动删除。
String queueName = channel.queueDeclare().getQueue();
在创建了消息队列和 类型的交换器之后,我们需要将两者进行绑定,让交换器将消息发送 给该队列。
channel.queueBind(queueName, "logs", "");
交换器类型
RabbitMQ 常用的交换器类型有 fanout、direct、topic、headers 这四种。
fanout
它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
direct
direct 类型的交换器路由规则也很简单,它会把消息路由到那些 BindingKey 和 RoutingKey 完全匹配的队列中。
交换器的类型为 direct,如果我们发送一条消息,并在发送消息的时候设置 路由键为“warning”,则消息会路由到 Queue1 和 Queue2,对应的示例代码如下:
channel.basicPublish(EXCHANGE_NAME, "warning",MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

如果在发送消息的时候设置路由键为“info”或者“debug”,消息只会路由到 Queue2。如 果以其他的路由键发送消息,则消息不会路由到这两个队列中。
topic
前面讲到 direct 类型的交换器路由规则是完全匹配 BindingKey 和 RoutingKey,但是这种严 格的匹配方式在很多情况下不能满足实际业务的需求。topic 类型的交换器在匹配规则上进行了 扩展,它与 direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队 列中,但这里的匹配规则有些不同,它约定:
- RoutingKey 为一个点号“
.”分隔的字符串(被点号“.”分隔开的每一段独立的字符 串称为一个单词),如“com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”; - BindingKey 和 RoutingKey 一样也是点号“
.”分隔的字符串; - BindingKey 中可以存在两种特殊字符串“
*”和“#”,用于做模糊匹配,其中“#”用于匹配一个单词,“#”用于匹配多规格单词(可以是零个)。
以图中的配置为例:
- 路由键为“com.rabbitmq.client”的消息会同时路由到 Queue1 和 Queue2;
- 路由键为“com.hidden.client”的消息只会路由到 Queue2 中;
- 路由键为“com.hidden.demo”的消息只会路由到 Queue2 中;
- 路由键为“java.rabbitmq.demo”的消息只会路由到 Queue1 中;
- 路由键为“java.util.concurrent”的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。

headers
headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中 的 headers 属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时, RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全 匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由 到该队列。headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
RabbitMQ工作流程

生产者发送消息的过程
- 生产者连接到 RabbitMQ Broker,建立一个TCP连接(Connection),开启一个信道(Channel)
- 生产者声明一个Exchange(交换器),并设置相关属性,比如交换机类型、是否持久化等
- 生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除等
- 生产者通过 routingKey (路由Key)将交换器和队列绑定( binding )起来
- 生产者发送消息至RabbitMQ Broker,其中包含 routingKey (路由键)、交换器等信息
- 相应的交换器根据接收到的 routingKey 查找相匹配的队列。
- 如果找到,则将从生产者发送过来的消息存入相应的队列中。
- 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
- 关闭信道。
- 关闭连接。
消费者接收消息的过程
- 消费者连接到RabbitMQ Broker ,建立一个连接(Connection ) ,开启一个信道(Channel) 。
- 消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数, 以及做一些准备工作
- 等待RabbitMQ Broker 回应并投递相应队列中的消息, 消费者接收消息。
- 消费者确认( ack) 接收到的消息。
- RabbitMQ 从队列中删除相应己经被确认的消息。
- 关闭信道。
- 关闭连接。
如图所示,我们又引入了两个新的概念:Connection 和 Channel。我们知道无论是生产 者还是消费者,都需要和 RabbitMQ Broker 建立连接,这个连接就是一条 TCP 连接,也就是 Connection。一旦 TCP 连接建立起来,客户端紧接着可以创建一个 AMQP 信道(Channel),每 个信道都会被指派一个唯一的 ID。信道是建立在 Connection 之上的虚拟连接,RabbitMQ 处理 的每条 AMQP 指令都是通过信道完成的。

我们完全可以直接使用 Connection 就能完成信道的工作,为什么还要引入信道呢?试想这 样一个场景,一个应用程序中有很多个线程需要从 RabbitMQ 中消费消息,或者生产消息,那 么必然需要建立很多个 Connection,也就是许多个 TCP 连接。然而对于操作系统而言,建立和 销毁 TCP 连接是非常昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。RabbitMQ 采用 类似 NIO (Non-blocking I/O)的做法,选择 TCP 连接复用,不仅可以减少性能开销,同时也 便于管理。
工作模型

生产者(Producer):发送消息的应用;(java程序,也可能是别的语言写的程序)
消费者(Consumer):接收消息的应用;(java程序,也可能是别的语言写的程序)
代理(Broker):就是消息服务器,RabbitMQ Server就是Message Broker;
连接(Connection):连接RabbitMQ服务器的TCP长连接;
信道(Channel):连接中的一个虚拟通道,消息队列发送或者接收消息时,都是通过信道进行的;
虚拟主机(Virtual host):一个虚拟分组,在代码中就是一个字符串,当多个不同的用户使用同一个RabbitMQ服务时,可以划分出多个Virtual host,每个用户在自己的Virtual host创建exchange/queue等;(分类比较清晰、相互隔离)
交换机(Exchange):交换机负责从生产者接收消息,并根据交换机类型分发到对应的消息队列中,起到一个路由的作用;
路由键(Routing Key):交换机根据路由键来决定消息分发到哪个队列,路由键是消息的目的地址;
绑定(Binding):绑定是队列和交换机的一个关联连接(关联关系);
队列(Queue):存储消息的缓存;
消息(Message):由生产者通过RabbitMQ发送给消费者的信息;(消息可以任何数据,字符串、user对象,json串等等)
客户端开发向导
连接 RabbitMQ
下面的代码用来在给定的参数(IP 地址、端口号、用户名、密码等)下 连接 RabbitMQ:
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(USERNAME);
factory.setPassword(PASSWORD);
factory.setVirtualHost(virtualHost);
factory.setHost(IP_ADDRESS);
factory.setPort(PORT);
//创建连接
Connection conn = factory.newConnection();
//获取信道
Channel channel = conn.createChannel();
在创建之后,Channel 可以用来发送或者接收消息了。
使用交换器和队列
交换器和队列是 AMQP 中 high-level 层面的构建模块,应用程序需确保在使用它们的时候 就已经存在了,在使用之前需要先声明(declare)它们。
演示如何声明一个交换器和队列:
//创建交换器
channel.exchangeDeclare(exchangeName, "direct", true);
//创建临时队列,队列名称RabbitMQ 自动生成
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, exchangeName, routingKey);
上面创建了一个持久化的、非自动删除的、绑定类型为 direct 的交换器,同时也创建了一 个非持久化的、排他的、自动删除的队列(此队列的名称由 RabbitMQ 自动生成)。这里的交换 器和队列也都没有设置特殊的参数。
上面的代码也展示了如何使用路由键将队列和交换器绑定起来。上面声明的队列具备如下 特性:只对当前应用中同一个 Connection 层面可用,同一个 Connection 的不同 Channel 可共用,并且也会在应用连接断开时自动删除。
如果要在应用中共享一个队列,可以做如下声明,如代码所示。
channel.exchangeDeclare(exchangeName, "direct", true);
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化,默认消息存在内存中
* 3.该队列是否只供一个消费者进行消费,是否进行消息共享,true可以多个消费者消费,false只能一个消费者消费
* 4.是否自动删除,最后一个消费者断开连接以后,该队列是否自动删除,true自动删除,false不自动删除
* 5.其他参数
*/
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
这里的队列被声明为持久化的、非排他的、非自动删除的,而且也被分配另一个确定的已 知的名称(由客户端分配而非 RabbitMQ 自动生成)。
生产者和消费者都可以声明一个交换器或者队列。如果尝试声明一个已经存在的交换器或 者队列,只要声明的参数完全匹配现存的交换器或者队列,RabbitMQ 就可以什么都不做,并成 功返回。如果声明的参数不匹配则会抛出异常。
exchangeDeclare
exchangeDeclare 有多个重载方法,这些重载方法都是由下面这个方法中缺省的某些参 数构成的。
Exchange.DeclareOk exchangeDeclare(String exchange,String type, boolean durable,
boolean autoDelete, boolean internal,
Map<String, Object> arguments) throws IOException;
这个方法的返回值是 Exchange.DeclareOK,用来标识成功声明了一个交换器。
各个参数详细说明如下所述。
exchange:交换器的名称。type:交换器的类型,常见的如 fanout、direct、topicdurable:设置是否持久化。durable 设置为 true 表示持久化,反之是非持久化。持 久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。autoDelete:设置是否自动删除。autoDelete 设置为 true 则表示自动删除。自动 删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑 定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为:“当与此交换器 连接的客户端都断开时,RabbitMQ 会自动删除本交换器”。internal:设置是否是内置的。如果设置为 true,则表示是内置的交换器,客户端程 序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。argument:其他一些结构化参数,比如 alternate-exchange。
queueDeclare
queueDeclare 相对于 exchangeDeclare 方法而言,重载方法的个数就少很多,它只 有两个重载方法:
Queue.DeclareOk queueDeclare() throws IOException;
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive,
boolean autoDelete, Map<String, Object> arguments) throws IOException;
不带任何参数的 queueDeclare 方法默认创建一个由 RabbitMQ 命名的(类似这种 amq.gen-LhQz1gv3GhDOv8PIDabOXA 名称,这种队列也称之为匿名队列)、排他的、自动删除 的、非持久化的队列。
方法的参数详细说明如下所述。
queue:队列的名称。durable:设置是否持久化。为 true 则设置队列为持久化。持久化的队列会存盘,在 服务器重启的时候可以保证不丢失相关信息。exclusive:设置是否排他。为 true 则设置队列为排他的。如果一个队列被声明为排 他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意 三点:排他队列是基于连接(Connection)可见的,同一个连接的不同信道(Channel) 是可以同时访问同一连接创建的排他队列;“首次”是指如果一个连接已经声明了一个 排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;即使该队 列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列 适用于一个客户端同时发送和读取消息的应用场景。autoDelete:设置是否自动删除。为 true 则设置队列为自动删除。自动删除的前提是: 至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会 自动删除。不能把这个参数错误地理解为:“当连接到此队列的所有客户端断开时,这 个队列自动删除”,因为生产者客户端创建这个队列,或者没有消费者客户端与这个队 列连接时,都不会自动删除这个队列。arguments:设置队列的其他一些参数, 如 x-message-ttl、 x-expires、 x-max-length、x-max-length-bytes、x-dead-letter-exchange、x-deadletter-routing-key、x-max-priority 等。
queueBind
将队列和交换器绑定的方法如下,可以与前两节中的方法定义进行类比。
Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException;
Queue.BindOk queueBind(String queue, String exchange, String routingKey,
Map<String, Object> arguments) throws IOException;
void queueBindNoWait(String queue, String exchange,
String routingKey, Map<String, Object> arguments) throws IOException;
方法中涉及的参数详解。
queue:队列名称;exchange:交换器的名称;routingKey:用来绑定队列和交换器的路由键;argument:定义绑定的一些参数。
不仅可以将队列和交换器绑定起来,也可以将已经被绑定的队列和交换器进行解绑。
exchangeBind
我们不仅可以将交换器与队列绑定,也可以将交换器与交换器绑定,后者和前者的用法如 出一辙,相应的方法如下:
(1) Exchange.BindOk exchangeBind(String destination, String source, String routingKey) throws IOException;
(2) Exchange.BindOk exchangeBind(String destination, String source, String routingKey,
Map<String, Object> arguments) throws IOException;
(3) void exchangeBindNoWait(String destination, String source, String routingKey,
Map<String, Object> arguments) throws IOException;
方法中的参数可以参考exchangeDeclare 方法。绑定之后,消息从 source 交 换器转发到 destination 交换器,某种程度上来说 destination 交换器可以看作一个队列。
channel.exchangeDeclare("source", "direct", false, true, null);
channel.exchangeDeclare("destination", "fanout", false, true, null);
channel.exchangeBind("destination", "source", "exKey");
channel.queueDeclare("queue", false, false, true, null);
channel.queueBind("queue", "destination", "");
channel.basicPublish("source", "exKey", null, "exToExDemo".getBytes());
生产者发送消息至交换器 source 中,交换器 source 根据路由键找到与其匹配的另一个交换 器 destination,并把消息转发到 destination 中,进而存储在 destination 绑定的队列 queue 中

发送消息
如果要发送一个消息,可以使用 Channel 类的basicPublish 方法,比如发送一条内容 为“Hello World!”的消息,参考如下:
byte[] messageBodyBytes = "Hello, world!".getBytes();
channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
为了更好地控制发送,可以使用 mandatory 这个参数,或者可以发送一些特定属性的信息:
channel.basicPublish(exchangeName, routingKey, mandatory,
MessageProperties.PERSISTENT_TEXT_PLAIN, messageBodyBytes);
上面这行代码发送了一条消息,这条消息的投递模式(delivery mode)设置为 2,即消息会 被持久化(即存入磁盘)在服务器中。同时这条消息的优先级(priority)设置为 1,content-type 为“text/plain”。可以自己设定消息的属性:
channel.basicPublish(exchangeName, routingKey, new AMQP.BasicProperties.Builder()
.contentType("text/plain")
.deliveryMode(2)
.priority(1) .userId("hidden") .build()), messageBodyBytes);
也可以发送一条带有 headers 的消息:
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("localtion", "here");
headers.put("time","today");
channel.basicPublish(exchangeName, routingKey,
new AMQP.BasicProperties.Builder()
.headers(headers) .build()), messageBodyBytes);
还可以发送一条带有过期时间(expiration)的消息:
channel.basicPublish(exchangeName, routingKey,
new AMQP.BasicProperties.Builder()
.expiration("60000") .build()), messageBodyBytes);
对于 basicPublish 而言,有几个重载方法:
(1) void basicPublish(String exchange, String routingKey, BasicProperties props,
byte[] body) throws IOException;
(2) void basicPublish(String exchange, String routingKey, boolean mandatory,
BasicProperties props, byte[] body) throws IOException;
(3) void basicPublish(String exchange, String routingKey, boolean mandatory,
boolean immediate, BasicProperties props, byte[] body) throws IOException;
对应的具体参数解释如下所述。
exchange:交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串, 则消息会被发送到 RabbitMQ 默认的交换器中。routingKey:路由键,交换器根据路由键将消息存储到相应的队列之中。props:消息的基本属性集, 其包含 14 个属性成员, 分别有 contentType、contentEncoding、headers(Map<String,Object>)、deliveryMode、priority、 correlationId、replyTo、expiration、messageId、timestamp、type、userId、 appId、clusterId。其中常用的几种都在上面的示例中进行了演示。byte[]body:消息体(payload),真正需要发送的消息。- mandatory 和 immediate 的详细内容请参考后面
消费消息
rabbitMQ 的消费模式分两种:推(Push)模式和拉(Pull)模式。推模式采用 Basic.Consume 进行消费,而拉模式则是调用 Basic.Get 进行消费。
Channel 类中 basicConsume 方法有如下几种形式:
(5) String basicConsume(String queue, boolean autoAck, String consumerTag,boolean noLocal,
boolean exclusive,
Map<String, Object> arguments, Consumer callback) throws IOException;
其对应的参数说明如下所述。
- queue:队列的名称;
- autoAck:设置是否自动确认。建议设成 false,即不自动确认;
- consumerTag:消费者标签,用来区分多个消费者;
- noLocal:设置为 true 则表示不能将同一个 Connection 中生产者发送的消息传送给 这个 Connection 中的消费者;
- exclusive:设置是否排他;
- arguments:设置消费者的其他参数;
- callback:设置消费者的回调函数。 用来处理 RabbitMQ 推送过来的消息, 比如 DefaultConsumer,使用时需要客户端重写(override)其中的方法。
消费端的确认与拒绝
为了保证消息从队列可靠地达到消费者, RabbitMQ 提供了消息确认机制(message acknowledgement)。消费者在订阅队列时,可以指定 autoAck 参数,当 autoAck 等于 false 时,RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上 是先打上删除标记,之后再删除)。当 autoAck 等于 true 时,RabbitMQ 会自动把发送出去的 消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。
采用消息确认机制后,只要设置 autoAck 参数为 false,消费者就有足够的时间处理消息 (任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为 RabbitMQ 会一直 等待持有消息直到消费者显式调用 Basic.Ack 命令为止。
当 autoAck 参数置为 false,对于 RabbitMQ 服务端而言,队列中的消息分成了两个部分: 一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认 信号的消息。如果 RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者已经 断开连接,则 RabbitMQ 会安排该消息重新进入队列,等待投递给下一个消费者,当然也有可 能还是原来的那个消费者。
RabbitMQ 不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的 唯一依据是消费该消息的消费者连接是否已经断开,这么设计的原因是 RabbitMQ 允许消费者 消费一条消息的时间可以很久很久。
在消费者接收到消息后,如果想明确拒绝当前的消息而不是确认,那么应该怎么做呢?
消费者客户端可以调用与其对 应的 channel.basicReject 方法来告诉 RabbitMQ 拒绝这个消息。
void basicReject(long deliveryTag, boolean requeue) throws IOException;
其中 deliveryTag 可以看作消息的编号,如果 requeue 参数设置为 true,则 RabbitMQ 会重新将这条消息存入 队列,以便可以发送给下一个订阅的消费者;如果 requeue 参数设置为 false,则 RabbitMQ 立即会把消息从队列中移除,而不会把它发送给新的消费者。
Basic.Reject 命令一次只能拒绝一条消息, 如果想要批量拒绝消息, 则可以使用Basic.Nack这个命令。消费者客户端可以调用 channel.basicNack 方法来实现,
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
multiple 参数 设置为 false 则表示拒绝编号为 deliveryTag 的这一条消息, 这时候 basicNack 和 basicReject 方法一样;multiple 参数设置为 true 则表示拒绝 deliveryTag 编号之前所 有未被当前消费者确认的消息。
RabbitMQ工作模式
工作原理图

配置阶段
- 将
Queue(消息队列) 和Exchange(交换机)通过RoutingKey(路由键)进行绑定。

生产者
- 通过
Exchange(交换机)和RoutingKey(路由键)唯一确认Queue(消息队列),推送消息内容。

消费者
- 根据
Queue(消息队列)名称,接收新消息内容。

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

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。
此时就可以使用work 模型,多个消费者共同处理消息处理,消息处理的速度就能大大提高了。
声明队列
@SpringBootConfiguration
public class RabbitMqConfig {
/**
* work队列名称
*/
public static final String WORK_MSG_QUEUE = "work.msg.queue";
/**
* 声明work队列
*
* @return
*/
@Bean
public Queue getWorkQueue() {
//参数一:队列名;参数二:是否持久化队列
return new Queue(WORK_MSG_QUEUE, true);
}
}
创建生产者
这次我们循环发送,模拟大量消息堆积现象。
@SpringBootTest
@RunWith(SpringRunner.class)
public class WorkMqTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private Queue queue;
@Test
public void send() {
for (int i = 0; i < 10; i++) {
// 消息
String message = "hello, message_";
rabbitTemplate.convertAndSend(queue.getName(), "这是一条工作队列消息" + message + i);
}
}
}
创建多个消费者
要模拟多个消费者绑定同一个队列,我们在consumer服务的SpringRabbitListener中添加2个新的方法:
@Component
public class RabbitMqConsumer {
@RabbitListener(queues = RabbitMqConfig.WORK_MSG_QUEUE)
public void listenWork1(String message) {
System.out.println("消费者一转发消息是:" + message);
}
@RabbitListener(queues = RabbitMqConfig.WORK_MSG_QUEUE)
public void listenWork2(String message) {
System.out.println("消费者二转发消息是:" + message);
}
}
测试:

可以看到两个消费者都成功消费量word队列中的消息。
Work模型的使用:
- 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
- 通过设置
spring.rabbitmq.listener.simple.prefetch来控制消费者预取的消息数量
Publish/Subscribe
在RabbitMQ中,生产者不是将消息直接发送给消息队列,实际上生产者根本不知道一个消息被发 送到哪个队列。
生产者将消息发送给交换器。交换器非常简单,从生产者接收消息,将消息推送给消息队列。交换器必须清楚地知道要怎么处理接收到的消息。应该是追加到一个指定的队列,还是追加到多个队列,还是丢弃。规则就是交换器类型。
在之前的测试案例中,都没有交换机,生产者直接发送消息到队列。而一旦引入交换机,消息发送的模式会有很大变化:

可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:
- Publisher:生产者,不再发送消息到队列中,而是发给交换机
- Exchange:交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
- Queue:消息队列也与以前一样,接收消息、缓存消息。不过队列一定要与交换机绑定。
- Consumer:消费者,与以前一样,订阅队列,没有变化
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
交换机的类型有四种:
- Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机
- Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
- Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
- Headers:头匹配,基于MQ的消息头匹配,用的较少。
Fanout交换机
Fanout,英文翻译是扇出,我觉得在MQ中叫广播更合适。
也叫扇形交换机,这种交换机没有 RoutingKey(路由键)概念,就算你绑定了路由键也是无视的。这个交换机在接收到消息后,会直接转发到绑定它上面的所有队列。
xchange -> Queue
在广播模式下,消息发送流程是这样的:

- 1) 可以有多个队列
- 2) 每个队列都要绑定到Exchange(交换机)
- 3) 生产者发送的消息,只能发送到交换机
- 4) 交换机把消息发送给绑定过的所有队列
- 5) 订阅队列的消费者都能拿到消息
队列
@SpringBootConfiguration
public class RabbitMqConfig {
//队列1名称
public static final String PUBLISH_MSG_QUEUE1 = "publish.msg.queue1";
//队列2名称
public static final String PUBLISH_MSG_QUEUE2 = "publish.msg.queue2";
//交换机名称
public static final String PUBLISH_EXCHANGE = "publish.exchange";
/**
* Publish队列
*
* @return
*/
@Bean
public Queue getPublishQueue1() {
//name:队列名字
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable。
// autoDelete:是否自动删除,当没有生产者或消费者使用此队列,该队列会自动删除。
return new Queue(PUBLISH_MSG_QUEUE1, true);
}
/**
* Publish队列
*
* @return
*/
@Bean
public Queue getPublishQueue2() {
return new Queue(PUBLISH_MSG_QUEUE2, true);
}
/**
* Fanout交换机
*
* @return
*/
@Bean
public FanoutExchange publishExchange() {
FanoutExchange exchange = new FanoutExchange(PUBLISH_EXCHANGE, true, false);
return exchange;
}
/**
* 将队列1和交换器绑定
*
* @return
*/
@Bean
public Binding bindPublishExchangeQueue1() {
Binding binding = BindingBuilder.bind(getPublishQueue1()).to(publishExchange());
return binding;
}
/**
* 将队列2和交换器绑定
*
* @return
*/
@Bean
public Binding bindPublishExchangeQueue2() {
Binding binding = BindingBuilder.bind(getPublishQueue2()).to(publishExchange());
return binding;
}
}
生产者
@SpringBootTest
class SpringRabbitmqApplicationTests {
@Resource
private RabbitTemplate rabbitTemplate;
@Autowired
private FanoutExchange fanoutExchange;
@Test
void contextLoads() {
for (int i = 0; i < 10; i++) {
//参数一:交换机名称;参数二:routingKey(广播模式不传);参数三:消息体
rabbitTemplate.convertAndSend(fanoutExchange.getName(), null, "这是一条工作队列消息" + i);
}
}
}
消费者
@Component
public class PublishConsumer {
@RabbitListener(queues = RabbitMqConfig.PUBLISH_MSG_QUEUE1)
public void listenDead1(String message) {
System.out.println("消费者1接收消息:" + message);
}
@RabbitListener(queues = RabbitMqConfig.PUBLISH_MSG_QUEUE2)
public void listenDead2(String message) {
System.out.println("消费者2接收消息:" + message);
}
}
测试:

可以看到两个消费之都接收了生产者所有的消息;与工作队列不同的是,工作队列的消费者只消费部分消息,而此模式是消费所有。
Direct交换机
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

在Direct模型下:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey(路由key) - 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey。 - Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key进行判断,只有队列的Routingkey与消息的Routing key完全一致,才会接收到消息
案例:
- 声明一个名为
hmall.direct的交换机 - 声明队列
direct.queue1,绑定hmall.direct,bindingKey为blud和red - 声明队列
direct.queue2,绑定hmall.direct,bindingKey为yellow和red - 在
consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2 - 在publisher中编写测试方法,向
hmall.direct发送消息
队列
@Configuration
public class RabbitDirectConfig {
@Bean
public Queue TestDirectQueue1() {
return new Queue("direct.queue1", true);
}
@Bean
public Queue TestDirectQueue2() {
return new Queue("direct.queue2", true);
}
@Bean
public DirectExchange TestDirectExchange() {
return new DirectExchange("hmall.direct", true, false);
}
/**
* 队列绑定路由key
* @return
*/
@Bean
Binding bindingDirect1() {
return BindingBuilder.bind(TestDirectQueue1()).
to(TestDirectExchange()).with("blue");//direct.queue1绑定路由blue
}
@Bean
Binding bindingDirect2() {
return BindingBuilder.bind(TestDirectQueue1()).
to(TestDirectExchange()).with("red");//direct.queue1绑定路由red
}
/**
* 队列绑定路由key
* @return
*/
@Bean
Binding bindingDirect3() {
return BindingBuilder.bind(TestDirectQueue2()).
to(TestDirectExchange()).with("yellow");//direct.queue2绑定路由yellow
}
@Bean
Binding bindingDirect4() {
return BindingBuilder.bind(TestDirectQueue2()).
to(TestDirectExchange()).with("red");//direct.queue2绑定路由red
}
}
生产者
@SpringBootTest
class SpringRabbitmqApplicationTests {
@Resource
private RabbitTemplate rabbitTemplate;
@Autowired
private DirectExchange directExchange;
@Test
void contextLoads() {
for (int i = 0; i < 10; i++) {
//生产者绑定路由key
rabbitTemplate.convertAndSend(directExchange.getName(), "red", "这是一条工作队列消息" + i);
}
}
}
消费者
@Component
public class DirectReceiver {
@RabbitListener(queues = "direct.queue1")
public void listenDirectQueue1(String msg) {
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "direct.queue2")
public void listenDirectQueue2(String msg) {
System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}
}
测试结果:

Topic交换机
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。
只不过Topic类型Exchange可以让队列在绑定BindingKey 的时候使用通配符!
BindingKey 一般都是有一个或多个单词组成,多个单词之间以.分割,例如: item.insert
通配符规则:
#:匹配0个或多个词*:匹配不多不少恰好1个词
举例:
item.#:能够匹配item.spu.insert或者item.spuitem.*:只能匹配item.spu
图示:

假如此时publisher发送的消息使用的RoutingKey共有四种:
china.news代表有中国的新闻消息;china.weather代表中国的天气消息;japan.news则代表日本新闻japan.weather代表日本的天气消息;
解释:
topic.queue1:绑定的是china.#,凡是以china.开头的routing key都会被匹配到,包括:china.newschina.weather
topic.queue2:绑定的是#.news,凡是以.news结尾的routing key都会被匹配。包括:china.newsjapan.news
队列
@Configuration
public class RabbitTopicConfig {
public static final String TOPIC_QUEUE_NAME_1 = "topic.queue1";
public static final String TOPIC_QUEUE_NAME_2 = "topic.queue2";
public static final String TOPIC_QUEUE_NAME_3 = "topic.queue3";
public static final String TOPIC_EXCHANGE_NAME = "testTopicExchange";
public static final String TOPIC_ROUTING_NAME_1 = "test.*";
public static final String TOPIC_ROUTING_NAME_2 = "china.#";
public static final String TOPIC_ROUTING_NAME_3 = "#.news";
@Bean
public Queue testTopicQueue1() {
return new Queue(RabbitTopicConfig.TOPIC_QUEUE_NAME_1);
}
@Bean
public Queue testTopicQueue2() {
return new Queue(RabbitTopicConfig.TOPIC_QUEUE_NAME_2);
}
@Bean
public Queue testTopicQueue3() {
return new Queue(RabbitTopicConfig.TOPIC_QUEUE_NAME_3);
}
/**
* 交换机(Exchange) 描述:接收消息并且转发到绑定的队列,交换机不存储消息
*/
@Bean
TopicExchange testTopicExchange() {
return new TopicExchange(RabbitTopicConfig.TOPIC_EXCHANGE_NAME, true, false);
}
/**
* 綁定队列 testTopicQueue1() 到 testTopicExchange 交换机,路由键只接受完全匹配 test.topic1 的队列接受者可以收到消息
*/
@Bean
Binding bindingTestTopic1(Queue testTopicQueue1, TopicExchange testTopicExchange) {
return BindingBuilder.bind(testTopicQueue1).to(testTopicExchange)
.with(RabbitTopicConfig.TOPIC_ROUTING_NAME_1);
}
@Bean
Binding bindingTestTopic2(Queue testTopicQueue2, TopicExchange testTopicExchange) {
return BindingBuilder.bind(testTopicQueue2).to(testTopicExchange)
.with(RabbitTopicConfig.TOPIC_ROUTING_NAME_2);
}
@Bean
Binding bindingTestTopic3(Queue testTopicQueue3, TopicExchange testTopicExchange) {
return BindingBuilder.bind(testTopicQueue3).to(testTopicExchange)
.with(RabbitTopicConfig.TOPIC_ROUTING_NAME_3);
}
}
生产者
@SpringBootTest
class SpringRabbitmqApplicationTests {
@Resource
private RabbitTemplate rabbitTemplate;
@Autowired
private TopicExchange topicExchange;
@Test
void contextLoads() {
for (int i = 0; i < 10; i++) {
//生产者绑定路由key
rabbitTemplate.convertAndSend(topicExchange.getName(), "china.a.b.c.d", "这是一条工作队列消息" + i);
}
}
}
消费者
@Component
public class TopicReceiver {
@RabbitListener(queues = "topic.queue1")
public void listenTopicQueue1(String msg) {
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "topic.queue2")
public void listenTopicQueue2(String msg) {
System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}
@RabbitListener(queues = "topic.queue3")
public void listenTopicQueue3(String msg) {
System.out.println("消费者3接收到direct.queue2的消息:【" + msg + "】");
}
}
测试:

基于注解声明
我们可以看到使用@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。
例如,我们同样声明Direct模式的交换机和队列:
@Component
public class DirectReceiver {
@RabbitListener(bindings = @QueueBinding(value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}))
public void listenDirectQueue1(String msg) {
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg) {
System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}
}
是不是简单多了。
再试试Topic模式:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg){
System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg){
System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}
消息转换器
Spring的消息发送代码接收的消息体是一个Object:

而在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
只不过,默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
- 数据体积过大
- 有安全漏洞
- 可读性差
测试默认转换器
我们直接将消息发送到消费者。
1)创建测试队列
首先,我们在consumer服务中声明一个新的配置类:
利用@Bean的方式创建一个队列,
具体代码:
package com.itheima.consumer.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageConfig {
@Bean
public Queue objectQueue() {
return new Queue("object.queue");
}
}
注意,这里我们先不要给这个队列添加消费者,我们要查看消息体的格式。
重启consumer服务以后,该队列就会被自动创建出来了:

2)发送消息
我们在publisher模块的SpringAmqpTest中新增一个消息发送的代码,发送一个Map对象:
@Test
public void testSendMap() throws InterruptedException {
// 准备消息
Map<String,Object> msg = new HashMap<>();
msg.put("name", "柳岩");
msg.put("age", 21);
// 发送消息
rabbitTemplate.convertAndSend("object.queue", msg);
}
发送消息后查看控制台:

可以看到消息格式非常不友好。
配置JSON转换器
显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。
在publisher和consumer两个服务中都引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
注意,如果项目中引入了spring-boot-starter-web依赖,则无需再次引入Jackson依赖。
配置消息转换器,在publisher和consumer两个服务的启动类中添加一个Bean即可:
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jackson2JsonMessageConverter.setCreateMessageIds(true);
return jackson2JsonMessageConverter;
}
消息转换器中添加的messageId可以便于我们将来做幂等性判断。
此时,我们到MQ控制台删除object.queue中的旧的消息。然后再次执行刚才的消息发送的代码,到MQ的控制台查看消息结构:

消费者接收Object
我们在consumer服务中定义一个新的消费者,publisher是用Map发送,那么消费者也一定要用Map接收,格式如下:
@RabbitListener(queues = "object.queue")
public void listenSimpleQueueMessage(Map<String, Object> msg) throws InterruptedException {
System.out.println("消费者接收到object.queue消息:【" + msg + "】");
}
RabbitMQ高阶
生产者的可靠性
当消息的生产者将消息发送出去之后,消息 到底有没有正确地到达服务器呢?如果不进行特殊配置,默认情况下发送消息的操作是不会返 回任何信息给生产者的,也就是默认情况下生产者是不知道消息有没有正确地到达服务器。
首先,我们一起分析一下消息丢失的可能性有哪些。
消息从发送者发送消息,到消费者处理消息,需要经过的流程是这样的:

消息从生产者到消费者的每一步都可能导致消息丢失:
- 发送消息时丢失:
- 生产者发送消息时连接MQ失败
- 生产者发送消息到达MQ后未找到
Exchange - 生产者发送消息到达MQ的
Exchange后,未找到合适的Queue - 消息到达MQ后,处理消息的进程发生异常
- MQ导致消息丢失:
- 消息到达MQ,保存到队列后,尚未消费就突然宕机
- 消费者处理消息时:
- 消息接收后尚未处理突然宕机
- 消息接收后处理过程中抛出异常
综上,我们要解决消息丢失问题,保证MQ的可靠性,就必须从3个方面入手:
- 确保生产者一定把消息发送到MQ
- 确保MQ不会将消息弄丢
- 确保消费者一定要处理消息
这一章我们先来看如何确保生产者一定能把消息发送到MQ。
生产者重试机制
首先第一种情况,就是生产者发送消息时,出现了网络故障,导致与MQ的连接中断。
为了解决这个问题,SpringAMQP提供的消息发送时的重试机制。即:当RabbitTemplate与MQ连接超时后,多次重试。
修改publisher模块的application.yaml文件,添加下面的内容:
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
我们利用命令停掉RabbitMQ服务。
然后测试发送一条消息,会发现会每隔1秒重试1次,总共重试了3次。消息发送的超时重试机制配置成功了!
注意:当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的。
如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。
生产者消息确认机制
一般情况下,只要生产者与MQ之间的网路连接顺畅,基本不会出现发送消息丢失的情况,因此大多数情况下我们无需考虑这种问题。
不过,在少数情况下,也会出现消息发送到MQ之后丢失的现象,比如:
- MQ内部处理消息的进程发生了异常
- 生产者发送消息到达MQ后未找到
Exchange - 生产者发送消息到达MQ的
Exchange后,未找到合适的Queue,因此无法路由
针对上述情况,RabbitMQ提供了生产者消息确认机制,包括Publisher Confirm和Publisher Return两种。在开启确认机制的情况下,当生产者发送消息给MQ后,MQ会根据消息处理的情况返回不同的回执。
具体如图所示:

总结如下:
- 当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功
- 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
- 持久消息投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功
- 其它情况都会返回NACK,告知投递失败
其中ack和nack属于Publisher Confirm机制,ack是投递成功;nack是投递失败。而return则属于Publisher Return机制。
默认两种机制都是关闭状态,需要通过配置文件来开启。
实现生产者确认
开启生产者确认
在publisher模块的application.yaml中添加配置:
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
这里publisher-confirm-type有三种模式可选:
none:关闭confirm机制,默认值simple:同步阻塞等待MQ的回执correlated:MQ异步回调返回回执
一般我们推荐使用correlated,回调机制。
定义ReturnCallback
每个RabbitTemplate只能配置一个ReturnCallback,因此我们可以在配置类中统一设置。我们在publisher模块定义一个配置类:
内容如下:
package com.itheima.publisher.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returned) {
log.error("触发return callback,");
log.debug("exchange: {}", returned.getExchange());
log.debug("routingKey: {}", returned.getRoutingKey());
log.debug("message: {}", returned.getMessage());
log.debug("replyCode: {}", returned.getReplyCode());
log.debug("replyText: {}", returned.getReplyText());
}
});
}
}
定义ConfirmCallback
由于每个消息发送时的处理逻辑不一定相同,因此ConfirmCallback需要在每次发消息时定义。具体来说,是在调用RabbitTemplate中的convertAndSend方法时,多传递一个参数:

这里的CorrelationData中包含两个核心的东西:
id:消息的唯一标示,MQ对不同的消息的回执以此做判断,避免混淆SettableListenableFuture:回执结果的Future对象
将来MQ的回执就会通过这个Future来返回,我们可以提前给CorrelationData中的Future添加回调函数来处理消息回执:

我们新建一个测试,向系统自带的交换机发送消息,并且添加ConfirmCallback:
@Test
void testPublisherConfirm() {
// 1.创建CorrelationData
CorrelationData cd = new CorrelationData();
// 2.给Future添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
// 2.1.Future发生异常时的处理逻辑,基本不会触发
log.error("send message fail", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容
if(result.isAck()){ // result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执
log.debug("发送消息成功,收到 ack!");
}else{ // result.getReason(),String类型,返回nack时的异常描述
log.error("发送消息失败,收到 nack, reason : {}", result.getReason());
}
}
});
// 3.发送消息
rabbitTemplate.convertAndSend("hmall.direct", "q", "hello", cd);
}
执行结果如下:

可以看到,由于传递的RoutingKey是错误的,路由失败后,触发了return callback,同时也收到了ack。
当我们修改为正确的RoutingKey以后,就不会触发return callback了,只收到ack。
而如果连交换机都是错误的,则只会收到nack。
开启生产者确认比较消耗MQ性能,一般不建议开启。而且大家思考一下触发确认的几种情况:
- 路由失败:一般是因为RoutingKey错误导致,往往是编程导致
- 交换机名称错误:同样是编程错误导致
- MQ内部故障:这种需要处理,但概率往往较低。因此只有对消息可靠性要求非常高的业务才需要开启,而且仅仅需要开启ConfirmCallback处理nack就可以了。
备份交换机
当消息经过交换器准备路由给队列的时候,发现没有对应的队列可以投递信息,在rabbitmq中会默认丢弃消息,如果我们想要监测哪些消息被投递到没有对应的队列,我们可以用备用交换机来实现,可以接收备用交换机的消息,然后记录日志或发送报警信息。
注意:备用交换机一般使用fanout交换机
Map<String, Object> arguments = new HashMap<>();
//指定当前正常的交换机的备用交换机是谁
arguments.put("alternate-exchange", EXCHANGE_ALTERNATE);
//DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
return new DirectExchange(EXCHANGE, true, false, arguments);
案例:
@Configuration
public class ConfirmConfig {
//交换机
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
//队列
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//备份交换机
public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
//备份队列
public static final String BACKUP_QUEUE_NAME = "backup.queue";
//报警队列
public static final String WARNING_QUEUE_NAME = "warning.queue";
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue() {
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//声明确认队列绑定关系
@Bean
public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
//声明备份 Exchange
@Bean("backupExchange")
public FanoutExchange backupExchange() {
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//声明确认 Exchange 交换机的备份交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange() {
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
.withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
// 声明警告队列
@Bean("warningQueue")
public Queue warningQueue() {
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
// 声明报警队列绑定关系
@Bean
public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange
backupExchange) {
return BindingBuilder.bind(queue).to(backupExchange);
}
// 声明备份队列
@Bean("backQueue")
public Queue backQueue() {
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
// 声明备份队列绑定关系
@Bean
public Binding backupBinding(@Qualifier("backQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange backupExchange) {
return BindingBuilder.bind(queue).to(backupExchange);
}
}
// 报警消费者
@Component
@Slf4j
public class WarningConsumer {
//接受报警消息
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message){
String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}",msg);
}
}
持久化
消息到达MQ以后,如果MQ不能及时保存,也会导致消息丢失,所以MQ的可靠性也非常重要。
为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。为了保证数据的可靠性,必须配置数据持久化,包括:
- 交换机持久化
- 队列持久化
- 消息持久化
队列持久化
如果rabbitmq的服务器重启的话,那么rabbitmq服务器上未消费的消息理论上是不能删除的,所以我们需要考虑这个问题,这个需要设置初始化,在spring rabbitMq中,queue的初始化如下:
/**
* The queue is durable, non-exclusive and non auto-delete.
*
* @param name the name of the queue.
*/
public Queue(String name) {
this(name, true, false, false);
}
调用的方法是:
/**
* Construct a new queue, given a name, durability, exclusive and auto-delete flags.
* @param name the name of the queue.
* @param durable true if we are declaring a durable queue (the queue will survive a server restart)
* @param exclusive true if we are declaring an exclusive queue (the queue will only be used by the declarer's
* connection)
* @param autoDelete true if the server should delete the queue when it is no longer in use
*/
public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete) {
this(name, durable, exclusive, autoDelete, null);
}
durable默认是false的,那么表示在服务器重启的时候,服务器会持久化保留该消息。
@Bean
public Queue getPublishQueue1() {
//name:队列名字
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable。
// autoDelete:是否自动删除,当没有生产者或消费者使用此队列,该队列会自动删除。
return new Queue(PUBLISH_MSG_QUEUE1, true);
}
交换机持久化
根据具体的交换机,调用对应的方法:
public FanoutExchange(String name, boolean durable, boolean autoDelete) {
super(name, durable, autoDelete);
}
如果设置为 true,则开启持久化功能,持久化可以将交换器信息存储到磁盘,重启 RabbitMQ 服务后交换器信息不会丢失。
如果设置为 false,则不开启持久化功能,那么在 RabbitMQ 服务重启之后,相关的交换器元数据会丢失,不过消息不会丢失。只是不能将新的消息发送到该交换器中了。如果需要长期使用的交换器,建议将其设置为持久化。
@Bean
public FanoutExchange publishExchange() {
FanoutExchange exchange = new FanoutExchange(PUBLISH_EXCHANGE, true, false);
return exchange;
}
消息持久化
- 发送时消息持久化
- Message包含
- body:body为我们需要发送的消息具体内容,一般以json字符串发送,消费端再解析
- MessageProperties:为Message的一些额外的属性,做一些扩展作用
public void send(String message){
MessageProperties messageProperties = new MessageProperties();
//消息持久化 MessageDeliveryMode.PERSISTENT
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
messageProperties.setContentType("UTF-8");
Message message1 = new Message(message.getBytes(), messageProperties);
//发送
rabbitTemplate.convertAndSend("exchange.name", null, message1);
}
在开启持久化机制以后,如果同时还开启了生产者确认,那么MQ会在消息持久化以后才发送ACK回执,进一步确保消息的可靠性。
不过出于性能考虑,为了减少IO次数,发送到MQ的消息并不是逐条持久化到数据库的,而是每隔一段时间批量持久化。一般间隔在100毫秒左右,这就会导致ACK有一定的延迟,因此建议生产者确认全部采用异步方式。
消费者的可靠性
当RabbitMQ向消费者投递消息以后,需要知道消费者的处理状态如何。因为消息投递给消费者并不代表就一定被正确消费了,可能出现的故障有很多,比如:
- 消息投递的过程中出现了网络故障
- 消费者接收到消息后突然宕机
- 消费者接收到消息后,因处理不当导致异常
- ...
一旦发生上述情况,消息也会丢失。因此,RabbitMQ必须知道消费者的处理状态,一旦消息处理失败才能重新投递消息。
但问题来了:RabbitMQ如何得知消费者的处理状态呢?
本章我们就一起研究一下消费者处理消息时的可靠性解决方案。
消费者确认机制
为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。即:当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:
- ack:成功处理消息,RabbitMQ从队列中删除该消息
- nack:消息处理失败,RabbitMQ需要再次投递消息
- reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
一般reject方式用的较少,除非是消息格式有问题,那就是开发问题了。因此大多数情况下我们需要将消息处理的代码通过try catch机制捕获,消息处理成功时返回ack,处理失败时返回nack.
由于消息回执的处理代码比较统一,因此SpringAMQP帮我们实现了消息确认。并允许我们通过配置文件设置ACK处理方式,有三种模式:
none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:- 如果是业务异常,会自动返回
nack; - 如果是消息处理或校验异常,自动返回
reject;
- 如果是业务异常,会自动返回
通过下面的配置可以修改SpringAMQP的ACK处理方式:
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none # 不做处理
修改consumer服务的SpringRabbitListener类中的方法,模拟一个消息处理的异常:
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
log.info("spring 消费者接收到消息:【" + msg + "】");
if (true) {
throw new MessageConversionException("故意的");
}
log.info("消息处理完成");
}
测试可以发现:当消息处理发生异常时,消息依然被RabbitMQ删除了。
我们再次把确认机制修改为auto:
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 自动ack
在异常位置打断点,再次发送消息,程序卡在断点时,可以发现此时消息状态为unacked(未确定状态):

放行以后,由于抛出的是消息转换异常,因此Spring会自动返回reject,所以消息依然会被删除:

我们将异常改为RuntimeException类型:
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
log.info("spring 消费者接收到消息:【" + msg + "】");
if (true) {
throw new RuntimeException("故意的");
}
log.info("消息处理完成");
}
在异常位置打断点,然后再次发送消息测试,程序卡在断点时,可以发现此时消息状态为unacked(未确定状态):

放行以后,由于抛出的是业务异常,所以Spring返回ack,最终消息恢复至Ready状态,并且没有被RabbitMQ删除:

当我们把配置改为auto时,消息处理失败后,会回到RabbitMQ,并重新投递到消费者。
失败重试机制
当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者。如果消费者再次执行依然出错,消息会再次requeue到队列,再次投递,直到消息处理成功为止。
极端情况就是消费者一直无法执行成功,那么消息requeue就会无限循环,导致mq的消息处理飙升,带来不必要的压力:

当然,上述极端情况发生的概率还是非常低的,不过不怕一万就怕万一。为了应对上述情况Spring又提供了消费者失败重试机制:在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
修改consumer服务的application.yml文件,添加内容:
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
重启consumer服务,重复之前的测试。可以发现:
- 消费者在失败后消息没有重新回到MQ无限重新投递,而是在本地重试了3次
- 本地重试3次以后,抛出了
AmqpRejectAndDontRequeueException异常。查看RabbitMQ控制台,发现消息被删除了,说明最后SpringAMQP返回的是reject
结论:
- 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
- 重试达到最大次数后,Spring会返回reject,消息会被丢弃
失败处理策略
在之前的测试中,本地测试达到最大重试次数后,消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下,显然不太合适了。
因此Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,它有3个不同实现:
RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。
1)在consumer服务中定义处理失败消息的交换机和队列
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
}
2)定义一个RepublishMessageRecoverer,关联队列和交换机
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
完整代码如下:
package com.itheima.consumer.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;
@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")
public class ErrorMessageConfig {
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
}
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
消息可靠性图解
消息的可靠性投递就是要保证消息投递过程中每一个环节都要成功,那么这肯定会牺牲一些性能,性能与可靠性是无法兼得的;
如果业务实时一致性要求不是特别高的场景,可以牺牲一些可靠性来换取性能。

- ① 代表消息从生产者发送到Exchange;
- ② 代表消息从Exchange路由到Queue;
- ③ 代表消息在Queue中存储;
- ④ 代表消费者监听Queue并消费消息;

1、确保消息发送到RabbitMQ服务器的交换机上
可能因为网络或者Broker的问题导致①失败,而此时应该让生产者知道消息是否正确发送到了Broker的exchange中;
有两种解决方案:
- 第一种是
开启Confirm(确认)模式;(异步) - 第二种是
开启Transaction(事务)模式;(性能低,实际项目中很少用)
2、确保消息路由到正确的队列
可能因为路由关键字错误,或者队列不存在,或者队列名称错误导致②失败。
使用return模式,可以实现消息无法路由的时候返回给生产者;
当然在实际生产环境下,我们不会出现这种问题,我们都会进行严格测试才会上线(很少有这种问题);
另一种方式就是使用备份交换机(alternate-exchange),无法路由的消息会发送到这个备用交换机上;
3、确保消息在队列正确地存储
可能因为系统宕机、重启、关闭等等情况导致存储在队列的消息丢失,即③出现问题;
解决方案:
- 队列持久化
- 交换机持久化
- 消息持久化
- 集群,镜像队列,高可用
- 确保消息从队列正确地投递到消费者
采用消息消费时的手动ack确认机制来保证;
如果消费者收到消息后未来得及处理即发生异常,或者处理过程中发生异常,会导致④失败。
为了保证消息从队列可靠地达到消费者,RabbitMQ提供了消息确认机制(message acknowledgement);
#开启手动ack消息消费确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
消费者在订阅队列时,通过上面的配置,不自动确认,采用手动确认,RabbitMQ会等待消费者显式地回复确认信号后才从队列中删除消息;
如果消息消费失败,也可以调用basicReject()或者basicNack()来拒绝当前消息而不是确认。如果requeue参数设置为true,可以把这条消息重新存入队列,以便发给下一个消费者(当然,只有一个消费者的时候,这种方式可能会出现无限循环重复消费的情况,可以投递到新的队列中,或者只打印异常日志);
过期时间(TTL)
TTL 是什么呢?
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
- 通过Queue属性设置,队列中所有消息都有相同的过期时间。
- 对消息自身进行单独设置,每条消息的TTL 可以不同。
如果两种方法一起使用,则消息的TTL 以两者之间较小数值为准。通常来讲,消息在队列中的生存 时间一旦超过设置的TTL 值时,就会变成“死信”(Dead Message),消费者默认就无法再收到该消息。当 然,“死信”也是可以被取出来消费的,下一小节我们会讲解。
队列统一过期
方法一:
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "boot_topic_exchange2";
public static final String QUEUE_NAME = "boot_queue666";
// 1 交换机
@Bean("bootExchange")
public Exchange bootExchange(){
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
}
//2.Queue 队列
@Bean("bootQueue")
public Queue bootQueue(){
return QueueBuilder.durable(QUEUE_NAME).ttl(10000).build();
}
//3. 队列和交互机绑定关系 Binding
/*
1. 知道哪个队列
2. 知道哪个交换机
3. routing key
noargs():表示不指定参数
*/
@Bean
public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue,
@Qualifier("bootExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
}
}
方法二:
在创建队列的时候设置队列的“x-message-ttl”属性
args.put("x-message-ttl", 10000);
消息过期
/**
* TTL:过期时间
* 1. 队列统一过期
* 2. 消息单独过期
* 如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。
*/
@Test
public void testMessageTtl() {
// 消息后处理对象,设置一些消息的参数信息
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//1.设置message的信息
// 第二个方法:消息的过期时间 ,5秒之后过期
message.getMessageProperties().setExpiration("5000");
//2.返回该消息
return message;
}
};
//消息单独过期
rabbitTemplate.convertAndSend("test_exchange_ttl","ttl.hehe","message ttl....",messagePostProcessor);
}
死信队列
DLX,全称为 Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当 消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个 交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
- 消费者使用
basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false - 消息是一个过期消息,超时无人消费
- 要投递的队列消息满了,无法投递
如果一个队列中的消息已经成为死信,并且这个队列通过**dead-letter-exchange属性指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机就称为死信交换机**(Dead Letter Exchange)。而此时加入有队列与死信交换机绑定,则最终死信就会被投递到这个队列中。
DLX 也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定,实 际上就是设置某个队列的属性。当这个队列中存在死信时,RabbitMQ 就会自动地将这个消息重 新发布到设置的 DLX 上去,进而被路由到另一个队列,即死信队列。
死信交换机有什么作用呢?
- 收集那些因处理失败而被拒绝的消息
- 收集那些因队列满了而被拒绝的消息
- 收集因TTL(有效期)到期的消息
//设置 x-dead-letter-exchange 参数来为这 个队列添加 DLX,参数 key 是固定值
args.put("x-dead-letter-exchange", " dlx_exchange ");
//可以为这个 DLX 指定路由键,如果没有特殊指定,则使用原队列的路由键,参数 key 是固定值
args.put("x-dead-letter-routing-key", "dlx-routing-key");

@Configuration
public class RabbitMQConfig {
/**
* 正常队列
*/
public static final String EXCHANGE = "boot-exchange";
public static final String QUEUE = "boot-queue";
public static final String ROUTING_KEY = "boot-rout";
/**
* 死信队列
*/
public static final String DEAD_EXCHANGE = "dead-exchange";
public static final String DEAD_QUEUE = "dead-queue";
public static final String DEAD_ROUTING_KEY = "dead-rout";
/**
* 声明死信交换机
*
* @return
*/
@Bean
public Exchange deadExchange() {
return ExchangeBuilder.directExchange(DEAD_EXCHANGE).build();
}
/**
* 声明死信队列
*
* @return
*/
@Bean
public Queue deadQueue() {
return QueueBuilder.durable(DEAD_QUEUE).build();
}
/**
* 绑定死信的队列和交换机
*
* @param deadExchange
* @param deadQueue
* @return
*/
@Bean
public Binding deadBind(Exchange deadExchange, Queue deadQueue) {
return BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTING_KEY).noargs();
}
/**
* 声明交换机,同channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.DIRECT);
*
* @return
*/
@Bean
public Exchange bootExchange() {
return ExchangeBuilder.directExchange(EXCHANGE).build();
}
/**
* 声明队列,同channel.queueDeclare(QUEUE, true, false, false, null);
* 绑定死信交换机及路由key
*
* @return
*/
@Bean
public Queue bootQueue() {
return QueueBuilder.durable(QUEUE)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey(DEAD_ROUTING_KEY)
//声明队列属性有更改时需要删除队列
//给队列设置消息时长
//.ttl(10000)
//队列最大长度
.maxLength(1)
.build();
}
/**
* 绑定队列和交换机,同 channel.queueBind(QUEUE, EXCHANGE, ROUTING_KEY);
*
* @param bootExchange
* @param bootQueue
* @return
*/
@Bean
public Binding bootBind(Exchange bootExchange, Queue bootQueue) {
return BindingBuilder.bind(bootQueue).to(bootExchange).with(ROUTING_KEY).noargs();
}
}
延迟消息
在电商的支付业务中,对于一些库存有限的商品,为了更好的用户体验,通常都会在用户下单时立刻扣减商品库存。例如电影院购票、高铁购票,下单后就会锁定座位资源,其他人无法重复购买。
但是这样就存在一个问题,假如用户下单后一直不付款,就会一直占有库存资源,导致其他客户无法正常交易,最终导致商户利益受损!
因此,电商中通常的做法就是:对于超过一定时间未支付的订单,应该立刻取消订单并释放占用的库存。
例如,订单支付超时时间为30分钟,则我们应该在用户下单后的第30分钟检查订单支付状态,如果发现未支付,应该立刻取消订单,释放库存。
但问题来了:如何才能准确的实现在下单后第30分钟去检查支付状态呢?
像这种在一段时间以后才执行的任务,我们称之为延迟任务,而要实现延迟任务,最简单的方案就是利用MQ的延迟消息了。
在RabbitMQ中实现延迟消息也有两种方案:
- 死信交换机+TTL
- 延迟消息插件
案例:创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:

@Configuration
public class TtlQueueConfig {
public static final String X_EXCHANGE = "X";
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
public static final String DEAD_LETTER_QUEUE = "QD";
// 声明 xExchange交换机
@Bean("xExchange")
public DirectExchange xExchange() {
return new DirectExchange(X_EXCHANGE);
}
// 声明 yExchange交换机
@Bean("yExchange")
public DirectExchange yExchange() {
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明队列 A ttl 为 10s 并绑定到对应的死信交换机
@Bean("queueA")
public Queue queueA() {
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
// 声明队列 A 绑定 X 交换机
@Bean
public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//声明队列 B ttl 为 40s 并绑定到对应的死信交换机
@Bean("queueB")
public Queue queueB() {
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
}
//声明死信队列 QD
@Bean("queueD")
public Queue queueD() {
return new Queue(DEAD_LETTER_QUEUE);
}
//声明死信队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange) {
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
消息生产者代码:
@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMsg/{message}")
public void sendMsg(@PathVariable String message) {
log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: " + message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: " + message);
}
}
消息消费者代码:
@Slf4j
@Component
public class DeadLetterQueueConsumer {
@RabbitListener(queues = "QD")//当队列已经存在时,直接指定队列名的方式消费
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
延迟消息
前面两种作用场景可以看做是把死信交换机当做一种消息处理的最终兜底方案,与消费者重试时讲的RepublishMessageRecoverer作用类似。
而最后一种场景,大家设想一下这样的场景:
如图,有一组绑定的交换机(ttl.fanout)和队列(ttl.queue)。但是ttl.queue没有消费者监听,而是设定了死信交换机hmall.direct,而队列direct.queue1则与死信交换机绑定,RoutingKey是blue:

假如我们现在发送一条消息到ttl.fanout,RoutingKey为blue,并设置消息的有效期为5000毫秒:

尽管这里的
ttl.fanout不需要RoutingKey,但是当消息变为死信并投递到死信交换机时,会沿用之前的RoutingKey,这样hmall.direct才能正确路由消息。
消息肯定会被投递到ttl.queue之后,由于没有消费者,因此消息无人消费。5秒之后,消息的有效期到期,成为死信:

死信被再次投递到死信交换机hmall.direct,并沿用之前的RoutingKey,也就是blue:

由于direct.queue1与hmall.direct绑定的key是blue,因此最终消息被成功路由到direct.queue1,如果此时有消费者与direct.queue1绑定, 也就能成功消费消息了。但此时已经是5秒钟以后了:

也就是说,publisher发送了一条消息,但最终consumer在5秒后才收到消息。我们成功实现了延迟消息。
DelayExchange插件
基于死信队列虽然可以实现延迟消息,但是太麻烦了。因此RabbitMQ社区提供了一个延迟消息插件来实现相同的效果。
官方文档说明:blog.rabbitmq.com/posts/2015/…
下载
插件下载地址:github.com/rabbitmq/ra…
由于我们安装的MQ是3.8版本,因此这里下载3.8.17版本:
安装
因为我们是基于Docker安装,所以需要先查看RabbitMQ的插件目录对应的数据卷。
docker volume inspect mq-plugins
结果如下:
[
{
"CreatedAt": "2024-06-19T09:22:59+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/mq-plugins/_data",
"Name": "mq-plugins",
"Options": null,
"Scope": "local"
}
]
插件目录被挂载到了/var/lib/docker/volumes/mq-plugins/_data这个目录,我们上传插件到该目录下。
接下来执行命令,安装插件:
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange
运行结果如下:

声明延迟交换机
基于注解方式:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = "delay"
))
public void listenDelayMessage(String msg){
log.info("接收到delay.queue的延迟消息:{}", msg);
}
基于@Bean的方式:
package com.itheima.consumer.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class DelayExchangeConfig {
@Bean
public DirectExchange delayExchange(){
return ExchangeBuilder
.directExchange("delay.direct") // 指定交换机类型和名称
.delayed() // 设置delay的属性为true
.durable(true) // 持久化
.build();
}
@Bean
public Queue delayedQueue(){
return new Queue("delay.queue");
}
@Bean
public Binding delayQueueBinding(){
return BindingBuilder.bind(delayedQueue()).to(delayExchange()).with("delay");
}
}
发送延迟消息
发送消息时,必须通过x-delay属性设定延迟时间:
@Test
void testPublisherDelayMessage() {
// 1.创建消息
String message = "hello, delayed message";
// 2.发送消息,利用消息后置处理器添加消息头
rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 添加延迟消息属性
message.getMessageProperties().setDelay(5000);
return message;
}
});
}
延迟消息插件内部会维护一个本地数据库表,同时使用Elang Timers功能实现计时。如果消息的延迟时间设置较长,可能会导致堆积的延迟消息非常多,会带来较大的CPU开销,同时延迟消息的时间会存在误差。
因此,不建议设置延迟时间过长的延迟消息。
幂等性
幂等是一个数学概念,用函数表达式来描述是这样的:f(x) = f(f(x)),例如求绝对值函数。
在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。例如:
- 根据id删除数据
- 查询数据
- 新增数据
但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果。比如:
- 取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况
- 退款业务。重复退款对商家而言会有经济损失。
所以,我们要尽可能避免业务被重复执行。
然而在实际业务场景中,由于意外经常会出现业务被重复执行的情况,例如:
- 页面卡顿时频繁刷新导致表单重复提交
- 服务间调用的重试
- MQ消息的重复投递
我们在用户支付成功后会发送MQ消息到交易服务,修改订单状态为已支付,就可能出现消息重复投递的情况。如果消费者不做判断,很有可能导致消息被消费多次,出现业务故障。
举例:
- 假如用户刚刚支付完成,并且投递消息到交易服务,交易服务更改订单为已支付状态。
- 由于某种原因,例如网络故障导致生产者没有得到确认,隔了一段时间后重新投递给交易服务。
- 但是,在新投递的消息被消费之前,用户选择了退款,将订单状态改为了已退款状态。
- 退款完成后,新投递的消息才被消费,那么订单状态会被再次改为已支付。业务异常。
因此,我们必须想办法保证消息处理的幂等性。这里给出两种方案:
- 唯一消息ID
- 业务状态判断
唯一消息ID
这个思路非常简单:
- 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
- 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
- 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
我们该如何给消息添加唯一ID呢?
其实很简单,SpringAMQP的MessageConverter自带了MessageID的功能,我们只要开启这个功能即可。
以Jackson的消息转换器为例:
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jjmc.setCreateMessageIds(true);
return jjmc;
}
业务判断
业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求或消息,不同的业务场景判断的思路也不一样。
例如我们当前案例中,处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行业务时判断订单状态是否是未支付,如果不是则证明订单已经被处理过,无需重复处理。
相比较而言,消息ID的方案需要改造原有的数据库,所以我更推荐使用业务判断的方案。
以支付修改订单的业务为例,我们需要修改OrderServiceImpl中的markOrderPaySuccess方法:
@Override
public void markOrderPaySuccess(Long orderId) {
// 1.查询订单
Order old = getById(orderId);
// 2.判断订单状态
if (old == null || old.getStatus() != 1) {
// 订单不存在或者订单状态不是1,放弃处理
return;
}
// 3.尝试更新订单
Order order = new Order();
order.setId(orderId);
order.setStatus(2);
order.setPayTime(LocalDateTime.now());
updateById(order);
}
上述代码逻辑上符合了幂等判断的需求,但是由于判断和更新是两步动作,因此在极小概率下可能存在线程安全问题。
我们可以合并上述操作为这样:
@Override
public void markOrderPaySuccess(Long orderId) {
// UPDATE `order` SET status = ? , pay_time = ? WHERE id = ? AND status = 1
lambdaUpdate()
.set(Order::getStatus, 2)
.set(Order::getPayTime, LocalDateTime.now())
.eq(Order::getId, orderId)
.eq(Order::getStatus, 1)
.update();
}
注意看,上述代码等同于这样的SQL语句:
UPDATE `order` SET status = ? , pay_time = ? WHERE id = ? AND status = 1
我们在where条件中除了判断id以外,还加上了status必须为1的条件。如果条件不符(说明订单已支付),则SQL匹配不到数据,根本不会执行。
兜底方案
虽然我们利用各种机制尽可能增加了消息的可靠性,但也不好说能保证消息100%的可靠。万一真的MQ通知失败该怎么办呢?
有没有其它兜底方案,能够确保订单的支付状态一致呢?
其实思想很简单:既然MQ通知不一定发送到交易服务,那么交易服务就必须自己主动去查询支付状态。这样即便支付服务的MQ通知失败,我们依然能通过主动查询来保证订单状态的一致。
流程如下:

图中黄色线圈起来的部分就是MQ通知失败后的兜底处理方案,由交易服务自己主动去查询支付状态。
不过需要注意的是,交易服务并不知道用户会在什么时候支付,如果查询的时机不正确(比如查询的时候用户正在支付中),可能查询到的支付状态也不正确。
那么问题来了,我们到底该在什么时间主动查询支付状态呢?
这个时间是无法确定的,因此,通常我们采取的措施就是利用定时任务定期查询,例如每隔20秒就查询一次,并判断支付状态。如果发现订单已经支付,则立刻更新订单状态为已支付即可。
定时任务大家之前学习过,具体的实现这里就不再赘述了。
至此,消息可靠性的问题已经解决了。
综上,支付服务与交易服务之间的订单状态一致性是如何保证的?
- 首先,支付服务会正在用户支付成功以后利用MQ消息通知交易服务,完成订单状态同步。
- 其次,为了保证MQ消息的可靠性,我们采用了生产者确认机制、消费者确认、消费者失败重试等策略,确保消息投递的可靠性
- 最后,我们还在交易服务设置了定时任务,定期查询订单支付状态。这样即便MQ通知失败,还可以利用定时任务作为兜底方案,确保订单支付状态的最终一致性
惰性队列
在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。但在某些特殊情况下,这会导致消息积压,比如:
- 消费者宕机或出现网络故障
- 消息发送量激增,超过了消费者处理速度
- 消费者处理业务发生阻塞
一旦出现消息堆积问题,RabbitMQ的内存占用就会越来越高,直到触发内存预警上限。此时RabbitMQ会将内存消息刷到磁盘上,这个行为成为PageOut. PageOut会耗费一段时间,并且会阻塞队列进程。因此在这个过程中RabbitMQ不会再处理新的消息,生产者的所有请求都会被阻塞。
为了解决这个问题,从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的模式,也就是惰性队列。惰性队列的特征如下:
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存(也就是懒加载)
- 支持数百万条的消息存储
而在3.12版本之后,LazyQueue已经成为所有队列的默认格式。因此官方推荐升级MQ为3.12版本或者所有队列都设置为LazyQueue模式。
配置Lazy模式
在利用SpringAMQP声明队列的时候,添加x-queue-mod=lazy参数也可设置队列为Lazy模式:
@Bean
public Queue lazyQueue(){
return QueueBuilder
.durable("lazy.queue")
.lazy() // 开启Lazy模式
.build();
}
这里是通过QueueBuilder的lazy()函数配置Lazy模式,底层源码如下:

当然,我们也可以基于注解来声明队列并设置为Lazy模式:
@RabbitListener(queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg){
log.info("接收到 lazy.queue的消息:{}", msg);
}
更新已有队列为lazy模式
对于已经存在的队列,也可以配置为lazy模式,但是要通过设置policy实现。
可以基于命令行设置policy:
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
命令解读:
rabbitmqctl:RabbitMQ的命令行工具set_policy:添加一个策略Lazy:策略名称,可以自定义"^lazy-queue$":用正则表达式匹配队列的名字'{"queue-mode":"lazy"}':设置队列模式为lazy模式--apply-to queues:策略的作用对象,是所有的队列