引言
现代应用程序中,消息队列已经成为高效、可靠和可扩展系统架构的重要组成部分。RabbitMQ作为一个流行的开源消息代理,提供了强大的消息传递功能,广泛应用于分布式系统、微服务架构和异步处理场景。通过使用RabbitMQ,开发者可以轻松地实现消息的发布与订阅、任务队列、负载均衡等多种模式,从而提高系统的性能和可维护性。
在这篇博客中,我们将深入探讨如何 使用官方提供的Java原生API连接RabbitMQ。我们将从基础知识开始,逐步深入了解RabbitMQ的核心概念,包括队列、交换机和消息的路由机制。下面将通过实际的代码示例,展示如何在Java应用程序中创建生产者和消费者,发送和接收消息。
若有疑问请参考官方文档:Java客户端基本操作RabbitMQ、Java客户端操作RabbitMQ
①:关于RabbitMQ部署和命令使用请参考:彻底掌握RabbitMQ部署和操作(超详细)
②:使用官方自带Java API 连接RabbitMQ:Java连接RabbitMQ(原生版)
③:将RabbitMQ客户端集成进SpringBoot(上):Java连接RabbitMQ(SpringBoot版·上)
④:将RabbitMQ客户端集成进SpringBoot(下):Java连接RabbitMQ(SpringBoot版·下)
本文核心代码请参考Gitee:所有案例完整代码
<!--导入RabbitMQ的Java客户端依赖坐标-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.25.0</version>
</dependency>
<!--构建新用户和虚拟空间;后面的所有案例以这个来操作-->
rabbitmqctl add_user rabbit 1234
rabbitmqctl set_user_tags rabbit administrator
rabbitmqctl add_vhost nativeStudy
rabbitmqctl set_permissions -p nativeStudy rabbit ".*" ".*" ".*"
| 类型 | 命名建议 | 示例 |
|---|---|---|
| Exchange 名称 | {业务}.{类型}.exchange | order.direct.exchange |
| Queue 名称 | {业务}.{操作}.queue | order.create.queue |
| RoutingKey | {业务}.{操作} | order.create |
🔸 类型可以是:
direct、topic、fanout、headers
🔸 避免使用拼音、无语义命名,如:test.queue、aaa.bbb、queue1等
⚠️ 说明:以下案例仅用于测试,未严格遵循命名规范!
一:普通队列(Standard Queue)
普通队列(Standard Queue)是RabbitMQ中最基本的队列类型,广泛用于实现消息的存储和传递。它是RabbitMQ的核心功能之一,支持多种消息传递模式。
在本小节中将使用Java编写两个程序来模拟普通队列,用生产者(Producer)发送消息到RabbitMQ队列后,再由消费者(Consumer)来监控RabbitMQ发送来的队列信息并进行消费;基本队列就是一个生产者发送消息到队列,监听那个队列的一个消费者获取消息并处理。
(一):案例代码
点开查看详情:工具类创建Channel连接通道(后文的连接通道我采用这个工具)
package cn.ant.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 创建信道的工具类
* @author Anhui AntLaddie (掘金蚂蚁小哥)
* @version 1.0
**/
public class ChannelUtil {
private static final String HOST = "***.***.***.***"; // 服务器IP
private static final String USER_NAME = "rabbit"; // 用户信息
private static final String PASSWORD = "1234"; // 用户密码
private static final String VIRTUAL_HOST = "nativeStudy"; // 虚拟主机
/***
* 获取连接中的一个信道
* @return Channel
*/
public static Channel getChannel() {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置RabbitMQ服务的IP、账号、密码、Vhost虚拟主机(默认 "/" 则不需要设置)
factory.setHost(HOST);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
factory.setVirtualHost(VIRTUAL_HOST);
try {
//通过工厂对象获取一个连接
Connection connection = factory.newConnection();
//通过连接来获取一个信道并返回
return connection.createChannel();
} catch (IOException | TimeoutException e) {
throw new RuntimeException(e);
}
}
}
点开查看详情:生产者(生产任务)
/**
* 生产者(生产任务)
* @author Anhui AntLaddie (掘金蚂蚁小哥)
* @version 1.0
**/
public class Producer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(Producer.class);
// 简单队列名称
public static final String QUEUE_NAME = "helloWorldQueue";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明队列(若重复执行相同参数的队列创建则不会执行任何操作,当队列第二次创建时参数不同则报错)
// 参数一:队列名称
// 参数二:队列信息是否持久化,true代表队列在RabbitMQ重启后仍然存在,默认false。
// 说明:这里是队列设置持久化,可不是消息持久化,消息持久化是后面设置的deliveryMode=2
// 参数三:指定队列是否为独占队列,该队列是否只供一个消费者进行消费的独占队列。
// true = 该队列只能由当前第一个连接的消费者使用,连接关闭后队列将被删除。
// false = 允许多个消费者连接到该队列(默认值)
// 参数四:指定队列是否在最后一个消费者断开连接后自动删除;
// true = 当最后一个消费者断开连接时,队列将被自动删除。
// false = 队列在消费者断开连接后仍然存在(默认值)。
// 参数五:构建队列的其它属性,可以用于设置队列的额外属性,例如消息 TTL(生存时间)、最大长度等.
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 发送的消息
byte[] msg = ("这是一条测试消息:" + LocalTime.now()).getBytes(StandardCharsets.UTF_8);
// 发送消息
// 参数一:要将消息发送到RabbitMQ服务器上的哪个交换机名称上。
// 说明:交换机负责接收来自生产者的消息,并根据路由规则将消息转发到一个或多个队列。
// 交换机的类型(如 `direct`、`fanout`、`topic`、`headers`)决定了消息的路由方式。
// 参数二:设置路由的key是什么;用于确定消息应该被路由到哪个队列
// 参数三:其它参数,如:内容类型、消息优先级、过期时间、消息持久性等
// 参数四:发送的消息的具体内容,通常是一个字节数组(byte[])
channel.basicPublish("", QUEUE_NAME, null, msg);
logger.info("生产者消息发送完成");
}
}
点开查看详情:消费者(消费任务)
/**
* 消费者(消费任务)
* @author Anhui AntLaddie (掘金蚂蚁小哥)
* @version 1.0
**/
public class Consumer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(Consumer.class);
// 简单队列名称
public static final String QUEUE_NAME = "helloWorldQueue";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
logger.info("消费者开始监听队列消息....");
// 推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) {
logger.info("获取队列信息:{}",
new String(message.getBody(), StandardCharsets.UTF_8));
}
};
// 取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) {
logger.info("监听的队列出现异常;可能队列被删除!{}", consumerTag);
}
};
//消费者消费消息
//参数一:要消费的队列的名称,消费者将从这个队列中接收消息。
// 说明:队列必须已经存在,在调用basicConsume方法之前,生产者已经向该队列发送了消息。
//参数二:指示消费者在接收到消息后是否自动发送应答
// true = 自动应答,消费者在接收到消息后,RabbitMQ 会自动将该消息标记为已处理。
// 这意味着即使消费者在处理消息时发生错误,消息也不会被重新投递。
// false = 手动应答,消费者需要在处理完消息后显式地发送应答(acknowledgment)。
// 如果消费者在处理消息时发生错误且未发送应答,RabbitMQ 会将该消息重新投递给其他消费者。
//参数三:这是一个实现了DeliverCallback接口的回调对象,用于处理接收到的消息。
// 说明:消费者从队列中接收到消息时,RabbitMQ会调用这个回调方法,可以在这里写处理消息的逻辑。
//参数四:这是一个实现了CancelCallback接口的回调对象,用于处理消费者取消消费的情况。
// 说明:当消费者被取消(如,队列被删除,或者消费者主动取消)时,
// RabbitMQ会调用这个回调方法。您可以在这里编写处理取消的逻辑。
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
注意:如果未指定队列名称,则会默认生成随机的队列名称。通过getQueue()方法获取名称。
若使用:channel.queueDeclare()方式创建等同于:queueDeclare("", false, true, true, null);
(二):实际效果
执行完生产者后会发现我们往消息队列中发布了一条消息(这里的生产者代码前半部分为创建队列);它只是一个普通队列,具体的队列创建和参数啥的请参考我之前的博文。
执行消费者代码后就会发现消费者会将之前的生产者提交的一条消息进行消费处理,随之队列的Messages都为0。现在只是简单案例还没涉及到一些手动确认啥的。
二:工作队列(Work Queues)
工作队列是一种利用普通队列实现工作队列模式的机制,将任务有效地分发给多个消费者,从而实现 负载均衡和并行处理。这种模式的核心思想是避免了立即执行资源密集型任务,防止系统因等待任务完成而造成的性能瓶颈。而工作队列允许我们将任务封装为消息,并将其发送到队列中,任务将在适当的时机被处理。
在工作队列中,后台运行的工作进程(消费者)会从队列中弹出任务并执行相应的作业。当有多个工作线程同时运行时,它们可以并行处理队列中的任务,从而提高系统的整体处理能力和响应速度。这种异步处理方式不仅优化了资源的使用,还能有效地应对高并发场景,确保系统在面对大量请求时依然能够保持稳定和高效。因此工作队列在现代分布式系统和微服务架构中被广泛应用,成为实现任务调度和负载均衡的重要工具。
如下生产者生产了1万个消息发送到队列中,这时为了提高处理效率往往设置了多个消费者同时监听消息队列并处理消息:
(一):案例代码
点开查看详情:生产者(生产任务)
/**
* 生产者(生产任务)
* @author Anhui AntLaddie (掘金蚂蚁小哥)
* @version 1.0
**/
public class Producer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(Producer.class);
// 工作队列名称
public static final String QUEUE_NAME = "WorkQueues";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明一个队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 循环发送消息(循环20条发生)
for (int i = 1; i <= 20; i++) {
String str = "这是一个编号为:" + i + " 的待处理的消息";
byte[] msg = (str + LocalTime.now()).getBytes(StandardCharsets.UTF_8);
channel.basicPublish("", QUEUE_NAME, null, msg);
}
logger.info("工作队列的生产者消息发送完成!");
}
}
点开查看详情:消费者A和B(消费任务)
/**
* 消费者A(消费任务)
* @author Anhui AntLaddie (掘金蚂蚁小哥)
* @version 1.0
**/
public class ConsumerA {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(ConsumerA.class);
// 工作队列名称
public static final String QUEUE_NAME = "WorkQueues";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 提前声明一个需要的队列,以防先启动消费者后发现队列不存在报错
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
logger.info("消费者A开始监听队列消息....");
// 消费者消费消息
channel.basicConsume(QUEUE_NAME, true, (consumerTag, message) -> {
// 获取从队列取出的数据并打印
String str = new String(message.getBody(), StandardCharsets.UTF_8);
// 这个消费者延迟1秒处理一条消息(不能理解为1秒执行一条后,再去队列获取哟)
try {
Thread.sleep(1000);
logger.info("A消费者获取队列信息并处理:{}", str);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, consumerTag -> {
logger.info("监听的队列出现异常;可能队列被删除!{}", consumerTag);
});
}
}
/**
* 消费者B(消费任务)
* @author Anhui AntLaddie (掘金蚂蚁小哥)
* @version 1.0
**/
public class ConsumerB {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(ConsumerB.class);
// 工作队列名称
public static final String QUEUE_NAME = "WorkQueues";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 提前声明一个需要的队列,以防先启动消费者后发现队列不存在报错
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
logger.info("消费者B开始监听队列消息....");
// 消费者消费消息
channel.basicConsume(QUEUE_NAME, true, (consumerTag, message) -> {
// 获取从队列取出的数据并打印
String str = new String(message.getBody(), StandardCharsets.UTF_8);
logger.info("B消费者获取队列信息并处理:{}", str);
}, consumerTag -> {
logger.info("监听的队列出现异常;可能队列被删除!{}", consumerTag);
});
}
}
(二):实际效果
创建完生产者和消费者后首先启动两个消费者,再启动生产者;这时生产者发送消息,被两个消费者监听并消费;当我们执行完生产者后会发现,消息是按照轮询的方式分配给消费者。第一个消息分配给消费者A,第二个消息分配给消费者B,第三个消息再分配给消费者A,以此类推。需要注意的是,现在还没使用到消费者手动确认消息哟,所以当生产者生成20条信息后会瞬间按照轮询的方式发送到消费者手上,队列不会关注消费者是否可以处理过来,发送过去就堆积到消费者那边等待线程执行,当数据量大时这里会有些问题。
三:工作队列之消息应答
每个消费者处理任务所需的时间可能不同。如果某个消费者在处理任务时仅完成了部分工作就突然挂掉了,会发生什么情况?那就会消息会丢失!因为RabbitMQ一旦向消费者传递了一条消息,便立即将该消息标记为删除。此外挂掉的消费者也无法接收后续发送给它的消息。为了消息在发送过程中不丢失,RabbitMQ引入了消息应答机制。通过这一机制,消费者在成功处理消息后会向RabbitMQ发送应答,只有在收到应答后,RabbitMQ才会将该消息标记为已删除,从而避免消息丢失的情况。
消息应答就是:消费者在接收到消息并且处理完该消息之后,消费者会告诉RabbitMQ它已经处理了,RabbitMQ再把刚才发送的消息删除。
(一):自动消息应答
在RabbitMQ中,消息发送后立即被认为已经传送成功,这种模式在高吞吐量和数据传输安全性之间需要进行权衡。如果在消费者接收到消息之前,消费者的连接或通道突然关闭了(如消费者服务器网络问题、资源耗尽崩溃、程序错误等),那么该消息将会丢失。另一方面,这种模式允许消费者接收过载的消息,没有对传递的消息数量进行限制,这可能导致消费者因接收过多尚未处理的消息而出现积压,最终导致内存耗尽,甚至被操作系统强制终止。因此,这种模式仅适用于消费者能够高效且以一定速率处理消息的情况,特别是对于业务处理较简单的消息。
自动应答模式是:队列向消费者发送消息后,消费者接收到消息即被视为成功应答,随后队列将删除对应的消息。这种机制虽然提高了消息处理的效率,但也增加了消息丢失的风险。
(二):手动消息应答
前面案例都是采用自动应答机制,这种机制虽然提高了消息处理的效率,但在消费者处理消息的过程中可能会导致消息丢失。为了确保在消息消费过程中不丢失消息,我们需要将自动应答改为手动应答。
手动应答机制流程:消息从消息队列发送给消费者时,消费者在完成消息处理后,向RabbitMQ发送一个应答,告知其已成功处理该消息。当RabbitMQ收到应答后,才会将该消息标记为已处理,并允许发送下一条消息给消费者。这种方式确保了消息在处理过程中不会丢失,即使消费者在处理过程中出现故障,RabbitMQ也能够将未应答的消息重新投递给其他可用的消费者,从而提高系统的可靠性和稳定性。
'手动应答分的几种情况'
'注:basicConsume消息接收方法中的autoAck参数必须为false才可以显示为手动确认'
'(1):手动应答basicAck(long deliveryTag, boolean multiple)'
消费者在处理消息的过程中出了错,就没有什么办法重新处理这条消息,所以要求我们处理消息成功后要确认消息;
当autoAck=false时,RabbitMQ会等待消费者手动发回ack信号后,才从内存(和磁盘,如果是持久化消息的话)
中移除消息。它采用消息确认机制,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程
挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者手动调用channel.basicAck为止。
对于RabbitMQ服务器端而言,如果服务器端一直没有收到消费者的ack信号,并且消费此消息的消费者已经断开
连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。
这里我们启动了手动确认后,就必须调用channel.basicAck方法进行确认,否则的话RabbitMQ会一直进行等待,
当我们这个消费者关闭后,RabbitMQ会将该条消息再发给对应的消费者进行消费,直到有消费者对该条消息进行消
费并应答完成。
'(2):手动拒绝basicReject(long deliveryTag, boolean requeue)'
拒绝deliveryTag对应的消息,第二个参数是否requeue,true则重新入队列,否则丢弃或者进入死信队列。
'(3):手动不确认basicNack(long deliveryTag, boolean multiple, boolean requeue)'
不确认deliveryTag对应的消息,第二个参数是否应用于多消息,第三个参数是否requeue;
注:与basicReject区别就是同时支持多个消息,可以nack该消费者先前接收未ack的所有消息。
nack后的消息也可能会再次被自己消费到。
'(4):手动恢复basicRecover(boolean requeue)'
当消费者希望重新处理所有未确认的消息时,可以调用此方法。
为true时,RabbitMQ会将这些消息放回队列,并且尽可能的将之前recover的消息投递给其他消费者消费;
为false时,RabbitMQ会将这些消息重新投递给自己。
'参数说明:'
'deliveryTag:消息的唯一标识符,用于标识要确认的消息。'
可以通过message.getEnvelope().getDeliveryTag()获取。
'requeue:消息是否重新入列。'
若为true则代表将消息重新入队;
若为false则代表丢弃该消息或将其发送到死信队列(如果配置了死信队列)
'multiple:是否批量应答,true代表批量应答。'
假设有个队列依次排列为1、2、3...10 (1最先出队,10最后出队);
为true发送1~5消息给消费者处理完都未确认:
当到第6时执行应答方法,并且multiple为true,则代表1~6都被批量应答。
为false发送1~5消息给消费者处理完都未确认:
当到第6时执行应答方法,并且multiple为true,则代表只有6被应答。
总结:当true则确认所有小于或等于该deliveryTag的消息;反之false则仅确认指定的消息。
注:在使用RabbitMQ的手动应答模式时,如果消费者在处理消息的过程中发生了异常错误(如:代码错误),而消费者还没有调用
basicAck 方法来确认该消息,那么这条消息将不会被确认,也不会被其他消费者处理(因为当前报错的消费者还一直和RabbitMQ连接着呢,也没告诉你是否确认)。为了避免这种情况,通常建议在处理消息时使用 try-catch 语句来捕获可能的异常(如上图就是个正确例子)。
四:RabbitMQ持久化
在生产环境中,服务器宕机是不可避免的,RabbitMQ也可能会因为某些特殊情况而出现宕机并重启。在这种情况下,消息队列中的数据,包括交换机、队列以及队列中的消息的恢复显得尤为重要。RabbitMQ提供了持久化机制,以确保这些数据的安全性。持久化的主要原理是将相关信息写入磁盘。当RabbitMQ服务宕机并重启后,它能够从磁盘中读取这些持久化的信息,从而恢复之前的数据。
(一):交换机持久化
默认不是持久化的,在服务器重启之后,交换机会消失。我们在管理台的Exchange页签下查看交换机,可以看到使用上述方法声明的交换机,Features一列是空的,即没有任何附加属性。注:后面会详讲交换机
当我们测试了使用持久化和非持久化方式创建了2个交换机后,尝试
rabbitmqctl stop来关闭RabbitMQ服务后再重启rabbitmq-server -detached后会发现非持久化的交换机没了。
(二):队列持久化及配置
与交换机的持久化相同,队列的持久化也是通过durable参数实现的(设置后队列的Features也会有个D),下面我将阐述一下关于队列创建的方法和基本参数说明:
创建队列持久化只需要将durable参数设置true即可。
'关于创建队列参数的补充说明:'
'我们创建队列常用的方法:'
'方法:queueDeclare(String queue,boolean durable,boolean exclusive,boolean autoDelete,Map<String, Object> arguments)'
'参数一:String queue'
队列的名称。如果指定的队列已经存在,则该方法不会重新创建队列,而是返回现有队列的名称。
注意:如果传入空字符串(""),RabbitMQ会为你生成一个唯一的队列名称。
'参数二:boolean durable'
参数跟交换机方法的参数一样,true表示做持久化,当RabbitMQ服务重启时,队列依然存在。
'参数三:boolean exclusive'
指定队列是否为独占队列。
若队列被声明为独占队列,那么这个队列只能由创建它的连接(connect)使用,并在连接断开的时候自动删除。
这里有三点必须说明:
1:排它队列的可见性
排它队列是基于连接可见的。同一连接中的不同信道(channel)可以同时访问由该连接创建的排它队列。
这意味着在同一连接内,多个信道可以共享对排它队列的访问权限。
2:连接创建限制
如果一个连接已经创建了一个排它队列,其他连接将无法创建同名的排它队列。
这一点与普通队列不同,普通队列可以被多个连接声明,只要它们的名称相同(名称、参数等都得相同才行)。
3:自动删除机制
即使排它队列被声明为持久化的,一旦创建它的连接关闭或客户端退出,该排它队列也会被自动删除。
这种特性使得排它队列非常适合用于单个客户端发送和读取消息的应用场景。
'参数四:boolean autoDelete'
自动删除,如果该队列没有任何订阅的消费者连接的话,该队列会被自动删除。这种队列适用于临时队列。
'参数五:Map<String, Object> arguments'
额外的参数,可以用于设置队列的其他属性,例如 TTL(生存时间)、最大长度等。
'######################### 常见的 arguments 键及说明: #########################'
'Ⅰ:x-message-ttl(消息过期时间,类型Integer)'
设置队列中消息的TTL(Time-To-Live),即消息在队列中的存活时间,单位是毫秒。
超过TTL的消息将被丢弃或者转发到死信队列(在配置了DLX的情况下生效)。
示例: xxx.put("x-message-ttl", 60000)(消息在队列中存活60秒)
'Ⅱ:x-expires(队列过期时间,类型Integer)'
设置队列的TTL,当队列没有被消费且超过了这个时间,队列会自动删除,单位是毫秒。
示例: xxx.put("x-expires", 60000)(队列过期时间为60秒)
补充:若超过60秒都没有消费者消费这个队列上的消息,那么这个队列将会过期销毁,
即使队列里有未被消费的延迟消息,这个队列都会被销毁。
'Ⅲ:x-dead-letter-exchange(死信交换机,类型String)'
设置消息过期或被拒绝后转发的交换机。如果设置了死信交换机,消息会被转发到指定的死信交换机。
示例:xxx.put("x-dead-letter-exchange", "dlx_exchange")(指定名为dlx_exchange死信交换机)
'Ⅳ:x-dead-letter-routing-key(死信路由键,类型String)'
设置死信消息的路由键,消息被转发到死信交换机时,使用此路由键。
示例:xxx.put("x-dead-letter-routing-key", "dlx_routing_key")
'Ⅴ:x-max-length (队列最大长度,类型Integer)'
设置队列中允许存储的最大消息数量。当队列中的消息数量超过此值时,
老的消息将会被丢弃(或者转发到死信队列,具体取决于配置)。
示例:xxx.put("x-max-length", 1000)(队列最多存储 1000 条消息)
'Ⅵ:x-max-priority(队列最大优先级,类型Integer)'
设置队列支持的最大优先级。如果配置了该值,队列就会支持消息的优先级。
消息的优先级由生产者指定,值越高的消息会优先被消费。
示例:xxx.put("x-max-priority", 10)(队列支持最大优先级为 10)
'Ⅶ:x-single-active-consumer(单一活跃消费者,类型Boolean)'
如果设置为true,队列中将只会有一个消费者在消费消息。
示例:xxx.put("x-single-active-consumer", true)
'Ⅷ:x-queue-mode(队列模式,类型String)'
设置队列的模式。可以选择的值有:
"default":常规队列模式
"lazy":懒加载模式,消息不会马上写入磁盘,适合用于持久化大量消息的场景。
示例:xxx.put("x-queue-mode", "lazy")
(三):消息持久化及配置
消息的持久化是指当消息从交换机发送到队列之后,被消费者消费之前,服务器突然宕机重启,消息仍然存在。消息持久化的前提是队列必须设置持久化,假如队列不是持久化,那么消息的持久化就毫无意义。通过如下代码设置消息的持久化:
'还记得当初生产者调用的发送消息的方法不:'
'方法:basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)'
'其实BasicProperties props就可以设置消息持久化方式:'
'参数实现类:'
public static class BasicProperties extends com.rabbitmq.client.impl.AMQBasicProperties {
private String contentType; // 消息的内容类型,如:text/plain
private String contentEncoding; // 消息内容编码
private Map<String,Object> headers; // 设置消息的header,类型为Map<String,Object>
private Integer deliveryMode; // 1(nopersistent)非持久化,2(persistent)持久化
private Integer priority; // 消息的优先级
private String correlationId; // 关联ID
private String replyTo; // 用于指定回复的队列的名称
private String expiration; // 消息的失效时间/毫秒
private String messageId; // 消息ID
private Date timestamp; // 消息的时间戳
private String type; // 类型
private String userId; // 用户ID
private String appId; // 应用程序ID
private String clusterId; // 集群ID
}
deliveryMode是设置消息持久化的参数,等于1不设置持久化,等于2设置持久化;
我们平时不会使用BasicProperties类而是使用MessageProperties封装类,通过这个类来获取具体配置。
'========================================================================================'
'####### 方式一:使用预定义常量'
比如设置MessageProperties.PERSISTENT_TEXT_PLAIN
本质代表:BasicProperties PERSISTENT_TEXT_PLAIN = new BasicProperties ("text/plain",
null,null,2,0, null, null, null,null, null, null, null,null, null);
'关于预定义MessageProperties常量说明:'
【MINIMAL_BASIC】:
最基本的消息属性配置,几乎没有设置任何属性。所有属性都是null或默认值。
适用于不需要任何特殊属性的消息。
【MINIMAL_PERSISTENT_BASIC】:
与MINIMAL_BASIC类似,但deliveryMode设置为2,这意味着消息是持久化的。
当你希望消息在RabbitMQ重启后仍然存在时使用。
【BASIC】:
描述:
contentType设置为"application/octet-stream",这是一个通用的二进制数据类型。
deliveryMode设置为1,表示消息是非持久化的。
priority设置为0(默认优先级)。
适用于传输二进制数据,并且不需要消息持久化的场景。
【PERSISTENT_BASIC】:
与BASIC类似,但deliveryMode设置为2,表示消息是持久化的。
当你希望传输的二进制数据在RabbitMQ重启后仍然存在时使用。
【TEXT_PLAIN】:
描述:
contentType设置为"text/plain",表示消息内容是纯文本。
deliveryMode设置为1,表示消息是非持久化的。
priority设置为0。
适用于传输纯文本消息,并且不需要消息持久化的场景。
【PERSISTENT_TEXT_PLAIN】:
与TEXT_PLAIN类似,但deliveryMode设置为2,表示消息是持久化的。
当你希望传输的纯文本消息在RabbitMQ重启后仍然存在时使用。
'####### 方式二:采用builder方式'
可以设置如发送消息的参数设置expiration过期时间、deliveryMode消息持久化方式等
AMQP.BasicProperties properties = new AMQP.BasicProperties()
.builder().expiration("10000").deliveryMode(2).build();
'注:通过方式二我们可以更灵活设置如:持久化、消息优先级、消息时间戳等等标记。'
注: 在RabbitMQ中,消息持久化是确保在服务器重启或异常崩溃时不会丢失重要信息的关键措施。然而,将所有消息都设置为持久化会显著降低系统性能,因为写入硬盘的速度远低于写入内存的速度。因此,
对于那些对可靠性要求不高的消息,可以选择不进行持久化处理,以提升整体的吞吐量。在决定是否将消息持久化时,需要在消息的可靠性和系统的吞吐量之间进行权衡,以找到一个适合应用需求的平衡点。
五:RabbitMQ消息分发
(一):不公平分发
在之前的案例中,RabbitMQ默认采用轮询策略进行消息分发,但在某些场景下这种策略并不理想。例如,当有两个消费者处理任务时,如果其中一个消费者(消费者A)处理速度非常快,而另一个(消费者B)处理速度较慢,轮询分发就会导致消费者A处理完任务后空闲,而消费者B仍在处理任务。消费者A需要等待消费者B处理完任务后才能接收到下一条消息,这不仅浪费了消费者A的资源,还可能导致服务器资源的低效利用,因为RabbitMQ并不知道这种情况,依然按照公平的原则进行分发。
为了解决这个问题,我们可以设置channel.basicQos(1)参数:这意味着如果消费者还没有处理完当前任务或未对其进行应答,RabbitMQ将不会再分配新的任务给这个消费者。换句话说,消费者一次只能处理一个任务。这样,RabbitMQ会将新任务分配给空闲的消费者。如果所有消费者都忙于处理任务,队列可能会因为持续增加的新任务而被撑满,此时可能需要添加新的消费者或者调整存储任务的策略。
这种策略实际上是一种不公平分发,更准确的描述是设置了预取值。预取值定义了信道中允许未确认消息的最大数量。如果预取值设置为1,处理速度快的消费者可以快速处理完任务并接收下一个,而处理速度慢的消费者则会继续处理当前任务,不再接收新的消息,从而实现不公平分发。
此外,在不公平分发下还需要设置手动应答模式, 因为自动应答模式下,虽然实现了不公平分发,但每个消费者消费的数据量可能仍然相近,因为自动应答意味着消息一旦发送给消费者即视为完成,RabbitMQ会继续向该消费者发送新消息。相反,手动应答模式下,消费速度慢的消费者需要完成当前任务并应答后,才会接收到下一条消息,这样,消费速度快的消费者将能够处理更多的消息,实现了真正的不公平分发。
如下图例可以发现,添加了分发机制后,消息只会等消费者消费完成以后才会接收到新的消息进行消费,这就能避免处理速度快的消费者空闲的情况,具体代码请参考Gitee的
QueuesMessageDistribution模块。
'不公平分发在RabbitMQ中的实现,是通过设置预取值(basicQos)和确认机制来实现的。'
'具体来说:'
- 当消费者设置了预取值(如basicQos(1)),意味着该消费者一次只能处理一个未确认的消息。
- 在消费者确认处理完当前任务后,RabbitMQ会立即将下一条消息分配给该消费者。
所以,处理速度较快的消费者将能够比慢的消费者处理更多的消息。
- 处理速度较慢的消费者在完成当前任务并确认后,才会接收到新的消息进行处理。
'总结:'
这种机制保证了消息的分发不是基于轮询,而是根据消费者的处理速度和确认情况动态调整,从而实现了不公平分发。
这有助于提高系统的整体效率,使得空闲的消费者能够及时处理更多的任务,而不会因为等待慢速消费者而浪费资源。
(二):预取值设置
RabbitMQ中的消息传输默认是异步进行的,因此在消费者连接到队列(队列有消息的情况下)的任何时刻,信道里通常会自动接收到队列里的不止一条消息。同时,来自消费者的消息确认(ACK)默认也是异步的。这就导致了存在一个未确认消息的缓冲区。为了防止缓冲区中未确认消息无限增长,开发者可以使用basicQos方法设置“预取计数”值来限制此缓冲区的大小。
预取计数值定义了信道上允许未确认消息的最大数量。当未确认消息数量达到basicQos设置的预设值时,RabbitMQ 将停止向该信道发送新的消息,直到至少有一个未处理的消息被确认(ACK)后,RabbitMQ才会继续发送新的消息。
比如信道上有未确认的消息标记为8,7,6,5,并且预取计数设置为4,此时RabbitMQ将不会再向该信道发送任何新消息,除非至少有一个消息被确认。比如标记为5的消息被确认了,RabbitMQ会检测到这个确认,并再向信道发送一条新消息9。
消息确认和QoS预取值对用户的吞吐量有重大影响。通常,增加预取计数会提高向消费者传递消息的速度。虽然自动确认模式下的消息传输速度是最佳的,但这样做会导致已传递但未处理的消息数量增加,从而增加消费者的RAM消耗。因此,应该谨慎使用自动确认模式或手动确认模式,特别是当消费者处理大量消息但未及时确认时,这会导致消费者连接节点的内存消耗增加。
找到合适的预取值是一个需要反复试验的过程。预取值的选择取决于不同的负载情况,通常来说,预取值在100到300的范围内,配合手动确认,可以提供最佳的吞吐量和数据安全,同时不会给消费者带来太大的风险。 预取值为1是最保守的选择,但这会使吞吐量变得很低,特别是在消费者连接延迟较高的情况下。对于大多数应用来说,稍微高一些的预取值会是最佳选择。
六:发布确认(Publisher Confirm)
生产者是可以将信道设置为确认模式(Confirm Mode)。一旦信道进入确认模式,所有通过该信道发布的消息都会被分配一个唯一的ID(从1开始)。消息成功投递到匹配的队列后,RabbitMQ(Broker)会向生产者发送一个包含消息唯一ID的确认指令,告知生产者消息已经正确到达目标队列。
如果消息和队列都被设置为持久化(Persistent),RabbitMQ会在向生产者确认消息之前,先将消息写入磁盘,然后再发送确认信息。 RabbitMQ发送给生产者的确认消息中包含了delivery-tag域,用于标识确认的消息序列号。此外还可以设置basic.ack的multiple域,以实现批量确认,表示当前序列号及其之前的所有消息都被确认。
其中异步确认模式的最大优势在于其异步特性。生产者在发布一条消息后,可以继续发送下一条消息,而无需等待确认。一旦消息得到确认,生产者可以通过回调方法处理该确认消息。如果RabbitMQ由于内部错误导致消息丢失,它会发送一个nack消息,生产者同样可以在回调方法中处理该nack消息。
这种模式不仅提高了生产者的效率,还增强了系统的可靠性和健壮性,使得消息传递更加可靠,即使在出现异常情况时也能及时得到通知。
'waitForConfirms系列方法用于处理消息确认(Publisher Confirm);'
'它们之间有一些细微的区别,主要体现在等待确认的方式和处理超时等方面:'
'Ⅰ:channel.waitForConfirms(timeout)'
作用:等待所有发布的消息得到确认,直到超时时间到达。
参数:timeout是一个整数,单位为毫秒,表示等待确认的最长时间。
行为:
如果在指定的超时时间内,所有消息都得到了确认,该方法返回true。
如果在超时时间内未能收到所有消息的确认,该方法返回false。
使用场景:
当需要对消息确认设置一个超时时间时使用。适用于需要对确认过程进行时间控制的情况。
如在某些场景下,生产者可能希望在一定时间内得到确认,如果超时则可以采取其他措施(如重试或记录日志)。
'Ⅱ:channel.waitForConfirms()'
作用:无限制地等待所有发布的消息得到确认。
行为:
这个方法会一直阻塞,直到所有发布的消息都得到确认。
如果任何一个消息未能被确认(如因网络问题或RabbitMQ内部错误),这个方法会抛出一个IOException。
使用场景:
当需要确保所有消息都被确认且不希望设置超时时间时使用。
在需要绝对保证消息传输可靠性的场景中,这是一个安全的选择,因为它会一直等待,直到所有消息都得到确认。
'Ⅲ:channel.waitForConfirmsOrDie()'
作用:无限制地等待所有发布的消息得到确认,如果出现任何问题则抛出异常。
行为:
与waitForConfirms()类似,但如果任何一个消息未能被确认(包括超时),
它会直接抛出一个IOException异常,而不是返回false。
使用场景:
在需要确保所有消息都被确认,并且在任何问题发生时立即得到通知的场景中使用。
适用于需要强制性确保消息传输成功的场景,如在事务性操作中。
'使用场景总结:'
waitForConfirms(timeout):当你需要对确认过程设置一个超时时间,并希望在超时后采取其他措施时使用。
waitForConfirms():当你需要无限制地等待所有消息确认,且不希望设置超时时间时使用。
waitForConfirmsOrDie():当你需要确保所有消息都得到确认,并且在任何问题发生时立即抛出异常以便处理时使用。
(一):发布确认开启
RabbitMQ的发布确认功能默认是关闭的。要启用此功能,生产者需要在信道(Channel)上调用confirmSelect()方法。每当你希望使用发布确认时,都必须在信道上调用该方法。
(二):单个发布确认
RabbitMQ提供了一种简单的发布确认方式同步确认。在这种模式下,生产者在发布一条消息后,必须等待RabbitMQ的回调确认方法被调用,并且生产者也接收到确认信息后,才能继续发布下一条消息。然而这种确认方式的最大缺点是发布速度较慢,因为如果没有收到对某条消息的确认,所有的后续消息发布都会被阻塞。这种方法的吞吐量通常不超过每秒数百条消息。对于某些应用来说,这种性能可能已经足够,但对于需要高吞吐量的情况,这可能会成为瓶颈。
点开查看详情:单个发布确认代码示例
public static void singleReleaseConfirmation() throws IOException, InterruptedException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明一个队列
channel.queueDeclare(SINGLE_RELEASE_CONFIRMATION, true, false, false, null);
// 开启发布确认功能!!!
channel.confirmSelect();
// 记录开始时间
long start = System.currentTimeMillis();
// 发送消息
for (int i = 1; i <= 100; i++) {
byte[] msg = ("这是一个编号为:" + i + " 的待处理的消息").getBytes(StandardCharsets.UTF_8);
// 依次发送每一条消息到消息队列中
channel.basicPublish("", SINGLE_RELEASE_CONFIRMATION, MessageProperties.PERSISTENT_BASIC, msg);
// 验证是否发送成功(阻塞等待队列返回的确认消息)
// 发送三秒后没得到回复将断定未发送过去
// boolean waitTime = channel.waitForConfirms(3000);
// 将一直等待刚才发送的消息是否被RabbitMQ确认
boolean waitSuccess = channel.waitForConfirms();
if (waitSuccess) {
// 如果选择的是info日志级别,所花的时间会高一点,因为日志级别为info,打印耗费一些时间
logger.info("[单个发布确认]发送成功了,已发送到RabbitMQ队列中,发送信息为:{}", i);
} else {
logger.info("[单个发布确认]发送失败,未收到RabbitMQ确认信息,发送信息为:{}", i);
}
}
long end = System.currentTimeMillis();
logger.info("生产者消息发送完成【单个发布确认,用时:{}】", end - start);
}
(三):批量发布确认
与逐条等待确认消息相比,先发布一批消息然后统一确认可以显著提高吞吐量。然而这种方法也存在一些缺点:当发生故障导致发布出现问题时,由于确认是批量进行的,我们无法精确知道是哪一条消息出现了问题。因此我们需要将整个批处理的消息保存在内存中,以记录关键信息,方便后续重新发布。需要注意的是,这种批量确认方案仍然是同步的,它同样会阻塞后续消息的发布。总之不推荐这样批量确认的方法使用。
点开查看详情:批量发布确认代码示例
public static void batchConfirmation() throws IOException, InterruptedException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明一个队列
channel.queueDeclare(BATCH_CONFIRMATION, true, false, false, null);
// 开启发布确认功能!!!
channel.confirmSelect();
// 记录开始时间
long start = System.currentTimeMillis();
// 定义每次批量处理多少消息进行确认
int batchNumber = 10;
// 发送的消息
for (int i = 1; i <= 100; i++) {
byte[] msg = ("这是一个编号为:" + i + " 的待处理的消息").getBytes(StandardCharsets.UTF_8);
// 依次发送每一条消息到消息队列中
channel.basicPublish("", BATCH_CONFIRMATION, MessageProperties.PERSISTENT_BASIC, msg);
// 验证是否发送成功,对10求余,代表每发送10条消息会批量看看是否成功(等待确认)
if (i % batchNumber == 0) {
boolean waitSuccess = channel.waitForConfirms();
// 计算,到时候日志需要打印
String str = (i - (batchNumber - 1)) + " ~ " + i;
// 判断是否批量确认成功
if (waitSuccess) {
// 如果选择的是info日志级别,所花的时间会高一点,因为日志级别为info,打印耗费一些时间
logger.debug("[批量发布确认]发送成功了,已发送到RabbitMQ队列中,发送信息范围:{}", str);
} else {
logger.debug("[批量发布确认]发送失败,未收到RabbitMQ确认信息,发送信息范围:{}", str);
}
}
}
long end = System.currentTimeMillis();
logger.info("生产者消息发送完成【批量发布确认,用时:{}】", end - start);
}
(四):异步确认发布(推荐)
异步确认虽然在编程逻辑上比同步确认和批量确认要复杂一些,但其性价比是最高的,无论是在可靠性还是效率方面都无可挑剔。它通过回调函数来确保消息的可靠传递。这种中间件也是通过回调函数来验证消息是否成功投递的。接下来,我们将详细讲解异步确认是如何实现的。
public static void asyncReleaseConfirmation() throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明一个队列
channel.queueDeclare(ASYNC_RELEASE_CONFIRMATION, true, false, false, null);
// 开启发布确认功能!!!
channel.confirmSelect();
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start
// 线程安全有序的一个哈希表Map,适用于高并发的情况
// 1.轻松的将序号与消息进行关联 2.轻松批量删除条目 只要给到序列号 3.支持并发访问
ConcurrentSkipListMap<Object, Object> outstandingConfirms = new ConcurrentSkipListMap<>();
// 消息确认成功,回调函数
// 回调成功后把集合中的确认数据删除,最终没删除的就代表是失败的
ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
// 是否为批量确认,当数据回调快的话是很有可能走批量确认的,具体看队列返回的回调情况
if (multiple) {
// 获取集合开头到deliveryTag数的数据记录,并组成新的集合,true代表包含当前这个数,false不包含
ConcurrentNavigableMap<Object, Object> navigableMap =
outstandingConfirms.headMap(deliveryTag, true);
navigableMap.clear();
} else {
// 若不批量则一个一个删除集合中的数据
outstandingConfirms.remove(deliveryTag);
}
logger.info("回调成功的数据:{} 是否批量确认:{}", deliveryTag, multiple);
};
// 消息确认失败,回调函数
ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
logger.info("回调失败的数据:{}", deliveryTag);
};
// 添加监听器,监听返回(监听器一定要再发送消息之前就创建和监听)
// 参数1:回调成功方法 参数2:回调失败方法
channel.addConfirmListener(ackCallback, nackCallback);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ End
// 记录开始时间
long start = System.currentTimeMillis();
// 发送的消息
for (int i = 1; i <= 100; i++) {
byte[] msg = ("这是一个编号为:" + i + " 的待处理的消息").getBytes(StandardCharsets.UTF_8);
// 把要发送的数据记录到集合中
outstandingConfirms.put(channel.getNextPublishSeqNo(), msg);
// 依次发送每一条消息
channel.basicPublish("", ASYNC_RELEASE_CONFIRMATION,
MessageProperties.PERSISTENT_TEXT_PLAIN, msg);
}
long end = System.currentTimeMillis();
logger.info("生产者消息发送完成【异步发布确认,用时:{}】", end - start);
// 到最后就可以处理发送确认失败的数据了
for (Map.Entry<Object, Object> outstandingConfirm : outstandingConfirms.entrySet()) {
// 在这里可以重新修改投递啥的
logger.debug("处理失败的消息:{}", outstandingConfirm);
}
}
}
七:RabbitMQ交换机(Exchange)
在RabbitMQ的消息传递模型中,交换机(Exchange)负责接收来自生产者的消息,并根据路由键(Routing Key)将消息转发到绑定的队列中。
RabbitMQ消息传递模型的核心思想是:生产者生产的消息不会直接发送到队列。 通常生产者甚至都不知道这些消息传递到了哪些队列中。相反,生产者只能将消息发送到交换机(exchange)。交换机的工作非常简单:一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息,比如:交换机把这些消息放到特定队列(需要有提前绑定路由键),还是说把它们群发到许多队列中,还是说应该丢弃它们。这就由交换机的类型来决定。
(一):绑定(bindings)
binding其实是exchange和queue之间的桥梁,它告诉我们exchange和那个队列进行了绑定关系。
(二):交换机类型
在RabbitMQ中,交换机(Exchange)是消息路由的关键组件。
RabbitMQ支持多种类型的交换机,每种类型的交换机都有其独特的消息路由逻辑。以下是RabbitMQ中常见的交换机类型:
'1.Direct Exchange(直接交换机)'
行为:将消息路由到绑定键(routing key)与队列名完全匹配的队列。
用途:适用于需要精确路由消息到特定队列的场景。
'2.Fanout Exchange(扇出交换机)'
行为:将消息发送到所有与此交换机绑定的队列,不考虑绑定键。
用途:用于需要将消息广播到多个消费者的场景,如日志系统。
'3.Topic Exchange(主题交换机)'
行为:基于绑定键的模式匹配,将消息路由到匹配的队列。绑定键可以包含通配符(如*和#)。
用途:适用于需要基于主题进行消息分发的情况,如发布订阅模式。
'4.Headers Exchange(标题交换机)'
行为:基于消息头中的键值对进行路由,而不是使用绑定键。适用于更复杂的路由需求。
用途:当需要根据消息的属性而不是路由键进行路由时使用。
'5.Default Exchange(默认交换机)'
行为:RabbitMQ有一个默认的无名交换机,它的行为类似于Direct Exchange,使用队列名作为绑定键。
用途:默认情况下,生产者可以直接将消息发送到队列,不需要明确指定交换机。
'6.System Exchange(特殊交换机)'
行为:这是一个特殊的交换机,用于将消息发送到RabbitMQ系统本身,如用于监控或管理目的。
用途:RabbitMQ内部使用,不常用于应用程序的消息传递。
'7.X-Delayed-Message Exchange(延迟交换机)'
行为:这是一种插件提供的交换机类型,可以延迟消息的投递。消息将在指定的时间后被发送到绑定的队列。
用途:用于实现消息的延迟投递。
注意事项
交换机的持久性:交换机可以配置为持久化(durable),以确保在RabbitMQ重启后仍然存在。
自动删除:交换机可以设置为自动删除,当所有与之绑定的队列都已删除时,交换机将被自动删除。
内部交换机:一些交换机是内部交换机,外部客户端不能直接发布消息到此交换机。
(三):无名交换机
'在之前的实现中,我们没有显式设置交换机,但仍然能够将消息发送到队列。'
'这是因为我们使用的是RabbitMQ的默认交换机,它由空字符串 ("") 表示。'
'之前调用basicPublish方法时:'
channel.basicPublish("", "队列名称", null, "发送的消息".getBytes());
参数一exchange为"",表示默认交换机(Default Exchange)。
参数二routingKey指定了目标队列的名称。
默认交换机是RabbitMQ提供的一个内置交换机,类型为direct(直接交换机)。它的特点是:
每个队列在声明时,都会自动绑定到默认交换机,绑定键(binding key)与队列名称相同。
因此消息的routingKey只需设置为目标队列的名称,消息就会路由到该队列。
'总结来说,使用默认交换机时,可以认为消息是直接路由到指定队列的,这一过程符合direct交换机的行为。'
(四):创建交换机
点开查看详情:创建交换机方法说明 exchangeDeclare(....)
'创建交换机的方法:'
void exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable,
boolean autoDelete, boolean internal, Map<String, Object> arguments)
'参数详细解释:'
'exchange(String):'
作用:指定要创建的交换机的名称。这个名称在RabbitMQ中必须是唯一的。
注意:如果交换机已经存在,exchangeDeclare方法会检查提供的参数是否与现有交换机的参数匹配。
如果不匹配,RabbitMQ会抛出错误。
'type(BuiltinExchangeType):'
作用:定义交换机的类型,决定了交换机如何路由消息。
选项:
BuiltinExchangeType.DIRECT:(默认交换机)消息只发送到与路由键完全匹配的队列。
BuiltinExchangeType.FANOUT:(扇出交换机)基于路由键的模式匹配,将消息路由到多个队列。
BuiltinExchangeType.TOPIC:(主题交换机)将消息广播到所有绑定到此交换机的队列。
BuiltinExchangeType.HEADERS:(标题交换机)基于消息头的匹配规则路由消息(不常用)。
'durable(boolean):'
作用:如果设置为true,交换机将被持久化,即RabbitMQ重启后,交换机仍然存在。
注意:持久化只保证交换机本身的持久性,不保证消息的持久性。
'autoDelete(boolean):'
作用:如果设置为true,当与此交换机的最后一个队列解绑时,交换机会被自动删除。
注意:这是一个“使用后删除”的策略,适用于临时交换机。
'internal(boolean):'
作用:若设置为true(默认false),交换机只能由RabbitMQ内部使用,客户端不能直接发布消息到这个交换机
用途:主要用于RabbitMQ的内部机制,如死信交换机。
'arguments (Map<String, Object>):'
作用:用于传递额外的参数,这些参数可以用来定制交换机的行为或实现AMQP协议的扩展功能。
常用参数:
alternate-exchange:指定一个备用交换机,当消息无法路由到任何队列时,消息会转发到这个备用交换机
x-dead-letter-exchange:指定一个死信交换机,当消息被拒绝或过期时,消息会转发到这个交换机
不管是生产者先启动还是消费者先启动,必须是先创建交换机,然后创建队列,最后将队列绑定到交换机上即可。具体原因如下:
交换机先于队列:因为交换机是消息路由的中心,队列需要知道它们应该连接到哪个交换机。队列先于绑定:因为绑定操作需要知道具体的队列和交换机。绑定操作:这是最后一步,因为它需要交换机和队列都已经存在,才能进行绑定。
八:Fanout扇出交换机(发布订阅模式)
扇出交换机(Fanout Exchange) 是RabbitMQ中最简单的交换机类型,其主要功能是广播消息。扇出交换机会将接收到的消息广播发送到所有与之绑定的队列,而忽略消息的路由键(routing key)。
因为不需要进行复杂的路由匹配或计算,扇出交换机的处理效率较高,是所有交换机类型中性能最快的之一。
扇出交换机特点:【广播行为】所有绑定到扇出交换机的队列都会接收到消息,无需额外的路由键逻辑。【高效简单】:由于不需要路由判断,扇出交换机在处理消息时性能较优。【应用场景】:适用于需要将相同消息发送给多个消费者的场景,例如日志分发、系统通知广播等。
点击查看详情:生产者(创建了交换机)
public class Producer {
//通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(Producer.class);
// 扇出交换机名称
public static final String EXCHANGE_NAME = "fanoutExchange";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 创建名称为"fanoutExchange"的扇出交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT,
true, false, false, null);
// 循环发送消息(循环20条发生)
for (int i = 1; i <= 20; i++) {
String str = "这是一个编号为:" + i + " 的待处理的消息";
byte[] msg = (str + LocalTime.now()).getBytes(StandardCharsets.UTF_8);
//发送消息到交换机 EXCHANGE_NAME
channel.basicPublish(EXCHANGE_NAME, "", MessageProperties.PERSISTENT_BASIC, msg);
}
logger.info("生产者消息发送完成!");
}
}点击查看详情:消费者(创建了交换机、队列、队列绑定交换机)
// 可以多搞几个消费者,消费者代码都是相同的,只不过日志打印的名称不一样
public class ConsumerA {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(ConsumerA.class);
// 扇出交换机名称
public static final String EXCHANGE_NAME = "fanoutExchange";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明exchange交换机 并设置发布订阅模式(扇出模式);防止消费者先启动报错,找不到交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT,
true, false, false, null);
// 创建一个临时队列(下面两种都是等同效果)
// String queueName = channel.queueDeclare("", false, true, true, null).getQueue();
String queueName = channel.queueDeclare().getQueue();
// 把队列绑定到指定的交换机上
channel.queueBind(queueName, EXCHANGE_NAME, "");
logger.info("消费者A开始监听队列消息....");
// 消费者消费消息
channel.basicConsume(queueName, false, (consumerTag, message) -> {
// 获取从队列取出的数据并打印
String str = new String(message.getBody(), StandardCharsets.UTF_8);
logger.info("A消费者获取队列信息并处理:{}", str);
// 手动应答,不批量应答
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {
logger.info("监听的队列出现异常;可能队列被删除!{}", consumerTag);
});
}
} 关于扇出交换机的核心就是:首先,创建一个扇出交换机,然后创建一个或多个队列。接下来,将这些队列绑定到扇出交换机上。当生产者将消息发送到这个扇出交换机时,交换机会根据其自身的绑定关系,将消息分发到所有绑定的队列中。通过这种方式,扇出交换机能够实现消息的广播发送,使得同一条消息可以被多个消费者接收。
九:Direct直接交换机(路由模式)
直接交换机(Direct Exchange) 是一种基于路由键的交换机,允许多个队列绑定到同一个交换机上,每个绑定都需要指定一个特定的路由键。当生产者将消息发送到直接交换机时,必须指定一个路由键,交换机会根据这个路由键将消息精确地路由到所有匹配的队列中,从而实现了消息的有针对性分发。如果有多个队列匹配,消息将被发送到所有这些队列,从而实现了消息的有针对性分发。
下面我将以一个案例的方式来使用直接交换机,如下图:有一个日志交换机(LogExchange),它负责的功能是将生产者发送的日志信息交到对应的队列中,队列分别为基本日志队列(BasicLogQueue)、错误队列(ErrQueue)、通知队列(NotifyQueue);其中基本日志队列记录日常运行日志,而错误队列记录重大问题信息,因为错误日志需要告知管理员,所有将错误日志又发送到通知队列来发送邮件告知。
点开查看详情:生产者代码(生产者内部代码创建了交换机)
public class Producer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(Producer.class);
// 直接交换机名称
public static final String LOG_EXCHANGE = "LogExchange";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 创建名称为"LogExchange"的直接交换机
channel.exchangeDeclare(LOG_EXCHANGE, BuiltinExchangeType.DIRECT,
true, false, false, null);
// 待发送的消息
HashMap<String, List<String>> sendMsg = new HashMap<>();
List<String> errMsg = Arrays.asList("[101]系统存在重大问题,可能会发生宕机!", "[102]电脑受到蠕虫病毒攻击!");
List<String> basicMsg = Arrays.asList("[201]尊敬的蚂蚁小哥欢迎登录系统", "[202]蚂蚁小哥已退出账号");
sendMsg.put("ErrKey", errMsg);
sendMsg.put("BasicLogKey", basicMsg);
// 循环发送消息任务
for (Map.Entry<String, List<String>> msg : sendMsg.entrySet()) {
String key = msg.getKey(); // 路由key
List<String> messages = msg.getValue(); // 待发送的消息
for (String message : messages) {
channel.basicPublish(LOG_EXCHANGE, key, MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes(StandardCharsets.UTF_8));
}
}
logger.info("生产者消息发送完成...");
}
}
点开查看详情:三个消费者代码(消费者内部创建了队列,且通过路由Key绑定到队列)
// BasicLogConsumer(基本日志队列)
public class BasicLogConsumer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(BasicLogConsumer.class);
// 交换机名称
public static final String LOG_EXCHANGE = "LogExchange";
// 队列名称
public static final String BASIC_LOG_QUEUE = "BasicLogQueue";
// 路由绑定关系 Routing Key
public static final String BASIC_LOG_KEY = "BasicLogKey";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明exchange交换机,并设置为直接交换机;防止消费者先启动报错,找不到交换机
// channel.exchangeDeclare(LOG_EXCHANGE, BuiltinExchangeType.DIRECT,
// true, false, false, null);
// 创建一个基本日志队列
channel.queueDeclare(BASIC_LOG_QUEUE, true, false, false, null);
// 基本日志队列绑定到交换机上,并通过路由key将交换机和队列关联起来。
channel.queueBind(BASIC_LOG_QUEUE, LOG_EXCHANGE, BASIC_LOG_KEY);
logger.info("BasicLogConsumer(基本日志队列)开始监听队列消息....");
// 接收队列消息
channel.basicConsume(BASIC_LOG_QUEUE, true, (consumerTag, message) -> {
logger.info("BasicLogConsumer(基本日志队列)获取队列信息并处理:{}",
new String(message.getBody(), StandardCharsets.UTF_8));
}, consumerTag -> {
logger.info("监听的队列(BasicLogConsumer)出现异常;可能队列被删除!{}", consumerTag);
});
}
}
// ErrConsumer(错误队列)
public class ErrConsumer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(ErrConsumer.class);
// 交换机名称
public static final String LOG_EXCHANGE = "LogExchange";
// 队列名称
public static final String ERR_QUEUE = "ErrQueue";
// 路由绑定关系 Routing Key
public static final String ERR_KEY = "ErrKey";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明exchange交换机,并设置为直接交换机;防止消费者先启动报错,找不到交换机
// channel.exchangeDeclare(LOG_EXCHANGE, BuiltinExchangeType.DIRECT,
// true, false, false, null);
// 创建一个错误队列
channel.queueDeclare(ERR_QUEUE, true, false, false, null);
// 错误队列绑定到交换机上,并通过路由key将交换机和队列关联起来。
channel.queueBind(ERR_QUEUE, LOG_EXCHANGE, ERR_KEY);
logger.info("ErrConsumer(错误队列)开始监听队列消息....");
// 接收队列消息
channel.basicConsume(ERR_QUEUE, true, (consumerTag, message) -> {
logger.info("ErrConsumer(错误队列)获取队列信息并处理:{}",
new String(message.getBody(), StandardCharsets.UTF_8));
}, consumerTag -> {
logger.info("监听的队列(ErrConsumer)出现异常;可能队列被删除!{}", consumerTag);
});
}
}
// NotifyConsumer(通知队列)
public class NotifyConsumer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(NotifyConsumer.class);
// 交换机名称
public static final String LOG_EXCHANGE = "LogExchange";
// 队列名称
public static final String NOTIFY_QUEUE = "NotifyQueue";
// 路由绑定关系 Routing Key
public static final String ERR_KEY = "ErrKey";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 声明exchange交换机 并设置为直接交换机;防止消费者先启动报错,找不到交换机
// channel.exchangeDeclare(LOG_EXCHANGE, BuiltinExchangeType.DIRECT,
// true, false, false, null);
// 创建一个通知队列
channel.queueDeclare(NOTIFY_QUEUE, true, false, false, null);
// 通知队列绑定到交换机上,并通过路由key将交换机和队列关联起来。
channel.queueBind(NOTIFY_QUEUE, LOG_EXCHANGE, ERR_KEY);
logger.info("NotifyConsumer(通知队列)开始监听队列消息....");
//接收队列消息
channel.basicConsume(NOTIFY_QUEUE, true, (consumerTag, message) -> {
logger.info("NotifyConsumer(通知队列)获取队列信息并处理:{}",
new String(message.getBody(), StandardCharsets.UTF_8));
}, consumerTag -> {
logger.info("监听的队列(NotifyConsumer)出现异常;可能队列被删除!{}", consumerTag);
});
}
}
从上面的描述可以看出,如果一个直接交换机(Direct Exchange)的多个队列绑定使用了相同的路由键(routing key),尽管绑定类型是direct,但它的行为会与扇出交换机(Fanout Exchange)类似,类似于广播消息到所有匹配的队列。
适用场景:直接交换机非常适合用于有优先级的任务处理。通过为不同优先级的任务设置不同的路由键(routing key),可以将消息发送到对应的队列中,从而实现资源的优先分配。例如,高优先级的任务可以被路由到拥有更多资源或更高处理能力的队列中,以确保这些任务能被快速处理。这样的设计不仅提高了任务处理的效率,还确保了关键任务能够及时响应和处理。
十:Topics主题交换机(匹配模式)
在之前的讨论中,我们介绍了扇出交换机(Fanout Exchange) ,它只能将消息广播到所有绑定的队列,而无法选择性地发送到特定队列。随后,我们引入了直接交换机(Direct Exchange) ,它允许根据确切的路由键(routing key)将消息发送到指定队列。然而,直接交换机也有其局限性:如果需要将一条消息发送到多个队列,那么交换机需要绑定大量的路由键,这会导致消息管理变得非常复杂。
为此,RabbitMQ提供了一种更为灵活的交换机类型——主题交换机(Topic Exchange) 。主题交换机允许消息在发送时携带符合特定模式的路由键。主题交换机会根据这些模式将消息分发到匹配的队列中,实现了模糊匹配的功能。这意味着,消息可以被发送到多个符合模式的队列,而不需要为每个队列单独设置一个路由键。这种设计大大简化了消息路由的复杂性,使得消息的分发更加智能和高效。
'主题交换机(Topic Exchange)的routing_key必须遵循一定的规则,以实现基于模式的模糊匹配。这些规则如下:'
- 点号(.)分隔:
routing_key由多个部分组成,每个部分用.(点号)分隔。
如:routing_key 可以是 quick.orange.rabbit 或 lazy.brown.fox 。
- 通配符:
*(星号):代表一个单词。
如:*.rabbitmq.* 会匹配 quick.rabbitmq.jump 和 lazy.rabbitmq.hop,
但不会匹配 quick.rabbitmq 或 rabbitmq.jump;因为示例必须遵循3个单词的组合。
#(井号):代表零个或多个单词。
如:rabbitmq.# 会匹配 rabbitmq 或 rabbitmq.quick 或 rabbitmq.quick.jump等。
- 匹配规则:
绑定键(binding key)和routing_key必须有相同的部分数目。
如:若队列的绑定键是 *.rabbitmq.*,则 routing_key 必须至少包含三个部分。
'注:绑定键中的 * 只能匹配一个单词,而 # 可以匹配零个或多个单词。'
'示例:如上图是一个队列绑定关系图,我们来看看他们之间数据接收情况是怎么样的:'
'队列绑定交换机的Key' '匹配规则'
quick.orange.rabbit 被队列 Q1 Q2 接收到
lazy.orange.elephant 被队列 Q1 Q2 接收到
quick.orange.fox 被队列 Q1 接收到
lazy.brown.fox 被队列 Q2 接收到
lazy.pink.rabbit 虽然满足两个绑定规则但两个规则都是在Q2队列,所有只有Q2接收一次
quick.brown.fox 不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit 是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit 是四个单词但匹配 Q2
在编写主题交换机的应用时,生产者在发送消息时必须将消息发送给主题交换机,并指定一个用点号(.)分割的路由键(routing_key),例如lazy.brown.fox。而消费者在创建队列并将其绑定到主题交换机时,必须使用包含匹配符(*或#)的绑定键(binding key)。如果不使用匹配符,队列的绑定行为会退化为直接交换机(Direct Exchange)或扇出交换机(Fanout Exchange)的模式:
直接交换机(Direct Exchange) :如果队列绑定到主题交换机时使用了一个没有匹配符的绑定键(例如lazy.brown.fox),它将只会接收到完全匹配这个绑定键的路由键的消息。这种行为实际上是直接交换机的特性,而不是退化为直接交换机。主题交换机仍然按照其规则进行匹配,只是这种情况下匹配规则非常严格,相当于直接交换机的精确匹配。
扇出交换机(Fanout Exchange) :若队列绑定到主题交换机时没有提供任何绑定键或使用了空字符串作为绑定键,那么这个队列将接收到主题交换机上的所有消息。此情况下,主题交换机的匹配规则被忽略,但这并不是退化为扇出交换机。它只是主题交换机的一种特殊用法,相当于忽略了路由键的匹配逻辑。
还有需要注意的是:当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像fanout扇出交换机了;如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是direct直接交换机了;
点开查看详情:生产者代码(主题交换机的生产者代码)
public class Producer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(Producer.class);
// 主题交换机名称
public static final String TOPIC_EXCHANGE = "TopicExchange";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 创建名称为"LogExchange"的直接交换机
channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC,
true, false, false, null);
// 准备需要发送的消息任务
HashMap<String, String> sendMsg = new HashMap<>();
sendMsg.put("quick.orange.rabbit", "被队列 Q1 Q2 接收到");
sendMsg.put("lazy.orange.elephant", "被队列 Q1 Q2 接收到");
sendMsg.put("quick.orange.fox", "被队列 Q1 接收到");
sendMsg.put("lazy.brown.fox", "被队列 Q2 接收到");
sendMsg.put("lazy.pink.rabbit", "虽然满足两个绑定规则但两个规则都是在Q2队列,所有只要Q2接收一次");
sendMsg.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
sendMsg.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
sendMsg.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2");
// 循环发送消息任务
for (Map.Entry<String, String> msg : sendMsg.entrySet()) {
String routKey = msg.getKey(); // 主题路由key
String message = msg.getValue();// 消息任务
channel.basicPublish(TOPIC_EXCHANGE, routKey, MessageProperties.PERSISTENT_BASIC,
message.getBytes(StandardCharsets.UTF_8));
}
logger.info("生产者消息发送完成...");
}
}
点开查看详情:消费者代码(两种主题的消费者代码)
// 消费者A;队列名Q1Queue;匹配*.orange.*主题;
public class CAConsumer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(CAConsumer.class);
// 交换机名称
public static final String TOPIC_EXCHANGE = "TopicExchange";
// 队列Q1名称
public static final String Q1 = "Q1Queue";
// 路由绑定关系(Routing Key)
public static final String Q1_KEY = "*.orange.*";
public static void main(String[] args) throws IOException {
// 调用自己的工具类获取信道
Channel channel = ChannelUtil.getChannel();
// 声明exchange交换机 并设置为主题交换机;防止消费者先启动报错,找不到交换机
channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC,
true, false, false, null);
// 创建一个基本日志队列
channel.queueDeclare(Q1, true, false, false, null);
// 队列绑定到交换机上,并通过路由key来对应两者的连接
channel.queueBind(Q1, TOPIC_EXCHANGE, Q1_KEY);
logger.info("CAConsumer(消费者A)开始监听Q1队列消息....");
// 接收队列消息
channel.basicConsume(Q1, false, (consumerTag, message) -> {
// 当前接收的消息
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
// 当前从哪个路由key上获取的消息
String routingKey = message.getEnvelope().getRoutingKey();
// 当前从哪个交换机里分配的消息
String exchange = message.getEnvelope().getExchange();
logger.info("CAConsumer(消费者A)获取队列信息并处理:{},绑定的路由名称:{}," +
"分发过来的路由名称:{},哪个交换机分发过来的消息:{}", msg, Q1_KEY, routingKey, exchange);
// 手动确认
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {
logger.info("CAConsumer(消费者A)出现异常;可能队列被删除!{}", consumerTag);
});
}
}
// 消费者B;队列名Q2Queue;匹配 *.*.rabbit 和 lazy.#主题;
public class CBConsumer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(CBConsumer.class);
// 交换机名称
public static final String TOPIC_EXCHANGE = "TopicExchange";
// 队列Q2名称
public static final String Q2 = "Q2Queue";
// 路由绑定关系(Routing Key 1)
public static final String Q2_KEY_A = "*.*.rabbit";
// 路由绑定关系(Routing Key 2)
public static final String Q2_KEY_B = "lazy.#";
public static void main(String[] args) throws IOException {
// 调用自己的工具类获取信道
Channel channel = ChannelUtil.getChannel();
// 声明exchange交换机 并设置为主题交换机;防止消费者先启动报错,找不到交换机
channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC,
true, false, false, null);
// 创建一个基本日志队列
channel.queueDeclare(Q2, true, false, false, null);
// 队列绑定到交换机上,并通过路由key来对应两者的连接(这里设置了2个路由连接)
channel.queueBind(Q2, TOPIC_EXCHANGE, Q2_KEY_A);
channel.queueBind(Q2, TOPIC_EXCHANGE, Q2_KEY_B);
logger.info("CBConsumer(消费者B)开始监听Q2队列消息....");
// 接收队列消息
channel.basicConsume(Q2, false, (consumerTag, message) -> {
// 当前接收的消息
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
// 当前从哪个路由key上获取的消息
String routingKey = message.getEnvelope().getRoutingKey();
// 当前从哪个交换机里分配的消息
String exchange = message.getEnvelope().getExchange();
logger.info("CBConsumer(消费者B)获取队列信息并处理:{},绑定的路由名称:{}," +
"分发过来的路由名称:{},哪个交换机分发过来的消息:{}",
msg, Q2_KEY_A + " AND " + Q2_KEY_B, routingKey, exchange);
// 手动确认
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {
logger.info("CBConsumer(消费者B)出现异常;可能队列被删除!{}", consumerTag);
});
}
}
十一:死信队列
死信队列(DLX, Dead Letter Exchange) 就是存储无法被正常消费的消息队列。一般来说,生产者将消息投递到RabbitMQ交换机(Exchange)或直接发送到队列(Queue),消费者从队列取出消息进行消费。但某些情况下,队列中的消息可能由于特定原因无法被消费,这些未被处理的消息称为死信。如果不进行额外的处理,这些失败消息将会被丢弃;但如果配置了死信队列(DLQ),这些死信(处理失败的消息)会被转发到死信交换机(DLX),再由交换机分发到指定的死信队列,供后续分析或处理。实际上,死信不会再被原来的消费者消费,但可以由特定的消费者专门处理。
应用场景:为了防止业务关键消息丢失,可以利用RabbitMQ的死信队列机制。当消息消费异常时,将其投递到死信队列。例如,用户在外卖平台下单后,如果在规定时间内没有完成支付,订单就会自动取消,系统会把这个取消订单的消息存入死信队列,以便后续记录或通知用户。
需要强调的是,如果RabbitMQ配置了死信队列,消息会被转移到死信队列中;如果未配置,消息将直接被丢弃。
(一):死信队列(DLX)产生来源
'Ⅰ:消息超时未消费(TTL 过期)'
队列或消息本身设置了存活时间(TTL, Time-To-Live),如果超过这个时间仍未被消费,消息就会变成死信。
// 示例:用户下单后,30分钟内未支付,订单作废,消息进入死信队列。
'Ⅱ:队列达到最大长度'
如果队列设置了最大长度,当队列已满且有新的消息到来时,最早的消息会被挤出,并作为死信存入死信队列。
// 示例:任务队列最多只能存1000条消息,当队列满了,新任务到来时,最旧的任务会被丢入死信队列。
'Ⅲ:消息被拒绝(Reject/Nack)且不重新入队'
消费者处理消息失败时,可以调用basic.reject或basic.nack拒绝该消息。
如果requeue=false(不重新入队),该消息会被标记为死信并存入死信队列。
// 示例:消费者因业务异常拒绝处理某条消息(如库存不足无法创建订单),此消息进入死信队列,供后续分析或重试。
(二):消息TTL过期产生死信
点开查看详情:消费死信队列的代码(死信队列的创建和消费)
public class DLXConsumer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(DLXConsumer.class);
// 声明死信交换机名称
public static final String DLX_EXCHANGE = "DLXExchange";
// 声明死信队列名称
public static final String DLX_QUEUE = "DLXQueue";
// 声明死信交换机到死信队列路由绑定关系(Routing Key)
public static final String DLX_KEY = "DLXKey";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 创建死信交换机(此时死信交换机为直接交换机(路由模式)直接路由到指定队列)
channel.exchangeDeclare(DLX_EXCHANGE, BuiltinExchangeType.DIRECT,
true, false, null);
// 创建死信队列,并设置队列持久化
channel.queueDeclare(DLX_QUEUE, true, false, false, null);
// 把死信队列绑定到死信交换机上
channel.queueBind(DLX_QUEUE, DLX_EXCHANGE, DLX_KEY);
logger.info("死信队列开始监听队列消息....");
// 接收消息;自动确认
channel.basicConsume(DLX_QUEUE, true, (consumerTag, message) -> {
logger.info("死信队列(DLXQueue)接收并处理本次消息:{}",
new String(message.getBody(), StandardCharsets.UTF_8));
}, (consumerTag) -> {
logger.info("监听的死信队列(DLXQueue)出现异常;可能队列被删除!{}", consumerTag);
});
}
}
点开查看详情:普通的消费者消费队列代码(消费失败的丢入死信队列)
public class Consumer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(Consumer.class);
// 直接交换机名称
public static final String NORMAL_EXCHANGE = "NormalExchange";
// 队列名称
public static final String NORMAL_QUEUE = "NormalQueue";
// 路由Key(Routing Key)
public static final String NORMAL_KEY = "NormalKey";
// 声明死信交换机名称
public static final String DLX_EXCHANGE = "DLXExchange";
// 声明死信交换机到死信队列路由绑定关系(Routing Key)
public static final String DLX_KEY = "DLXKey";
/***
* 正常的消费者,用来消息消息
*/
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 创建名称为"NormalExchange"的直接交换机;防止消费者先启动报错,找不到交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT,
true, false, false, null);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ 设置队列配置参数Start ~~~~~~~~~~~~~~~~~~~~~~~~~~
Map<String, Object> arguments = new HashMap<>();
// 正常队列设置死信交换机 参数key是固定值;(就是说死去的消息发送到哪个交换机)
arguments.put("x-dead-letter-exchange", DLX_EXCHANGE);
// 正常队列设置死信交换机到死信队列绑定的 Routing Key,参数key是固定值;
// 就是说死去的消息在交换机里通过什么路由发送到死信队列
arguments.put("x-dead-letter-routing-key", DLX_KEY);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ 设置队列配置参数End ~~~~~~~~~~~~~~~~~~~~~~~~~~
// 创建一个队列(其中arguments配置参数里面设置了死信交换机)
channel.queueDeclare(NORMAL_QUEUE, true, false, false, arguments);
// 把队列绑定到指定交换机上
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, NORMAL_KEY);
logger.info("消费者(TTLConsumer)开始监听队列消息....");
// ## 消息TTL过期产生死信
// 若不写从队列中取出消息消费的代码,那么就等消息在队列中自动过期,消息10秒过期(生产者中设置)
}
}
点开查看详情:生产者发送消息代码(将消息发送到交换机)
public class Producer {
// 通过日志管理器获取Logger对象
static Logger logger = LogManager.getLogger(Producer.class);
// 直接交换机名称
public static final String NORMAL_EXCHANGE = "NormalExchange";
// 路由Key
public static final String NORMAL_KEY = "NormalKey";
public static void main(String[] args) throws IOException {
// 通过工具类获取一个信道
Channel channel = ChannelUtil.getChannel();
// 创建名称为"NormalExchange"的直接交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT,
true, false, false, null);
// 设置需要发送的内容,并循环发送
List<String> msgContentList = Arrays.asList("a11", "b22", "a33", "b44", "a55");
msgContentList.forEach(e -> {
try {
// 创建消息,并每隔2秒发送消息到NORMAL_EXCHANGE交换机
byte[] msg = ("[消息:" + e + "]").getBytes(StandardCharsets.UTF_8);
Thread.sleep(2000);
// 消息参数配置信息;expiration过期时间10秒、deliveryMode=2为消息持久化
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.expiration("10000").deliveryMode(2).build();
// 发送消息
channel.basicPublish(NORMAL_EXCHANGE, NORMAL_KEY, properties, msg);
logger.info("生产者发送了一条消息,消息内容为:" + new String(msg));
} catch (Exception ex) {
logger.error("生产者消息发送失败×××");
throw new RuntimeException(ex.getMessage());
}
});
logger.info("生产者消息发送完成...");
}
}
测试(为了可以更好的演示效果,创建完死信队列后,先关闭死信消费者,等会启动):
执行生产者代码,这时生产者会发送5条消息到NormalExchange交换机,再由交换机发送到NormalQueue队列中,此时普通队列里面存在5条未消费信息(注意消息10秒过期哟,过期则会发送给死信交换机):
NormalQueue队列中的消息达到过期时间(10秒)后会从队列里推送到死信交换机里,再由死信交换机上绑定的路由Key来确定具体推送到哪个死信队列中。
接下来我们就可以启动死信消费者来消费这一批死信队列里的任务消息。
(三):队列达到最大长度产生死信
这里就简单了,还是之前的代码,但是需要剔除生产者代码中的消息过期时间,并在普通消费者里面设置队列最大长度,具体代码如下:
修改后需先删除原队列(因为队列参数改变,无法创建不同参数队列),再启动消费者,创建出带队列长度的队列
'## 改造生产者代码(关闭消息过期时间):'
// 消息参数配置信息;expiration过期时间10秒、deliveryMode=2为消息持久化
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
// .expiration("10000")
.deliveryMode(2).build();
'## 改造普通消费者代码(设置队列最大长度:arguments.put("x-max-length",3)):'
Map<String, Object> arguments = new HashMap<>();
// 正常队列设置死信交换机 参数key是固定值;(就是说死去的消息发送到哪个交换机)
arguments.put("x-dead-letter-exchange", DLX_EXCHANGE);
// 正常队列设置死信交换机到死信队列绑定的 Routing Key,参数key是固定值;
// 就是说死去的消息在交换机里通过什么路由发送到死信队列
arguments.put("x-dead-letter-routing-key", DLX_KEY);
// 设置正常队列的长度限制 为3(## 队列达到最大长度产生死信)
arguments.put("x-max-length",3);
(四):消息被拒产生死信
代码优化:删除普通消费者里面设置的队列最大长度配置,并编写普通消费者的手动确认消费代码。
下面代码的手动确认说明:就是消息文本中带'b'的都不会手动确认,不确认的会被推送到死信交换机。
logger.info("消费者(TTLConsumer)开始监听队列消息....");
// ## 消息TTL过期产生死信
// 若不写下面的从队列中取出消息消费的代码,那么就等消息在队列中自动过期,消息10秒过期(生产者中设置)
'~~~~~~~~~~~~~~~~ 添加如下代码 ~~~~~~~~~~~~~~~~'
// ## 消息被拒产生死信
// 接收消息
channel.basicConsume(NORMAL_QUEUE, false, (consumerTag, message) -> {
//## 消息处理(处理方式:若处理的消息内包含 "b" 字符串则拒绝处理,并设置手动不确认,并丢弃消息 )
//获取消息体
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
logger.info("消费者获取队列信息正在处理:{}", msg);
if (msg.contains("b")) {
//拒绝处理
logger.info("=======》 X 消息已被拒绝处理,并丢弃:{}", msg);
channel.basicNack(message.getEnvelope().getDeliveryTag(), false, false);
} else {
logger.info("=======》 √ 消息已被成功处理:{}", msg);
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
}, consumerTag -> {
logger.info("监听的队列出现异常;可能队列被删除!{}", consumerTag);
});
十二:延迟队列
普通队列:他是一种先进先出(FIFO)的数据结构,消息按照发送顺序进入队列,并按同样的顺序被消费者消费。消息一旦进入队列,就可以被消费者消费,但实际消息被消费的时间取决于队列的积压情况和消费者的处理能力。
延时队列:他的核心特性在于“延迟消费”。与普通队列不同,普通队列中的消息会尽快被处理,而延时队列中的消息需要在指定的时间后才可被消费。换句话说,延时队列用于存放需要在特定时间后才能被处理的消息或任务,消息在达到预设的延迟时间后,才会被推送给消费者执行。
点开查看:延迟队列的应用场景
'案例一:订单超时取消'
场景:电商平台中,用户下单后如果规定时间内未支付,订单应自动取消并释放库存。
实现:
● 订单被投递到延迟队列中,设置超时时间(如 30 分钟)。
● 超时时间到达后,消息被推送给消费者,触发订单取消逻辑。
补充:如何确保只取消未支付的订单?
在消费者处理这条超时消息时,必须再次检查订单状态,确保订单仍然是“未支付”状态,才执行取消逻辑。
否则,如果订单已经支付,则直接丢弃消息,不执行取消操作。
'案例二:秒杀库存超时释放'
场景:在限时秒杀活动中,用户下单后,系统会暂时锁定库存。
如果用户未在规定时间内完成支付,库存应自动释放,以便其他用户购买。
实现:
● 订单创建后,将订单信息投递到延迟队列,设置库存锁定时间(如 10 分钟)。
● 超时时间到达后,消息被推送给消费者,触发库存释放逻辑。
补充:如何确保库存不会被错误释放?
在消费者处理超时消息时,必须再次检查订单状态:
若订单仍然是“未支付”,则释放库存。
若订单已支付,则直接丢弃该消息,不执行释放操作。
'案例三:用户注册后发送激活邮件'
场景:用户注册后,需要在24小时内点击激活邮件完成账户激活,未激活的账户将被清理或提醒用户。
实现:
● 用户注册后,将账户信息投递到延迟队列,设置24小时延迟时间。
● 24小时后,消息被推送给消费者,检查账户状态:
若用户仍未激活,则发送激活提醒邮件或自动注销账户。
若用户已激活,则丢弃消息。
补充:如何避免误删已激活账户?
消费者在处理消息时,先查询数据库,确保用户状态仍然是“未激活”,才执行清理或提醒逻辑。
'案例四:社交平台的“消息撤回”功能'
场景:在社交软件(如微信、钉钉)中,用户发送消息后,允许在2分钟内撤回,超过时间则不可撤回。
实现:
● 用户发送消息后,将消息ID投递到延迟队列,设置2分钟延迟时间。
● 2分钟后,消息被推送给消费者,检查消息是否已撤回:
若用户未撤回,则从消息数据库移除撤回选项,用户不能再操作。
若用户已撤回,则丢弃消息,不做任何操作。
补充:如何保证用户体验?
采用前端倒计时 + 后端延迟队列双重控制,确保撤回功能准确生效。
'案例五:会员到期续费提醒'
场景:用户的VIP会员即将到期,系统需提前提醒用户续费,避免会员权益中断。
实现:
● 会员注册或续费成功后,投递到延迟队列,设置到期前3天的提醒时间。
● 延迟时间到达后,消息被推送给消费者,触发短信/邮件/APP推送提醒续费。
补充:如何确保不会重复提醒?
处理消息前,先检查用户是否已经续费:
若用户已续费,则丢弃消息,不再推送提醒。
若用户未续费,则发送提醒。
(一):实现延迟队列的两种方式
'在SpringBoot集成RabbitMQ会详细说明,或者看gitee代码'
'方式一:基于RabbitMQ的TTL(消息过期时间)+ 死信队列(DLX)'
'方式二:基于RabbitMQ的延迟插件(Delayed Message Exchange)'
需要安装RabbitMQ官方的rabbitmq_delayed_message_exchange插件。
TTL(Time-To-Live)是 RabbitMQ 中的一种属性,用于控制消息或者队列中所有消息的最大存活时间,单位为毫秒。具体来说:
消息 TTL:如果一条消息设置了 TTL 属性,或者进入了一个设置了 TTL 的队列,那么该消息将在指定的 TTL 时间内存活。如果消息未被消费,且超出了 TTL时间,则该消息会成为“死信”(Dead Letter)。
队列 TTL:如果设置了队列的 TTL,则该队列中所有消息的默认 TTL 将是队列 TTL 的值。如果队列中的消息在队列 TTL 到期前没有被消费,该消息将被认为是“死信”。
如果同时配置了消息的 TTL 和队列的 TTL,那么会取 较小的那个 TTL 值,即消息的 TTL 和队列的 TTL 中最短的一个值将被使用。
'设置这个TTL值有两种方式(队列设置、消息设置):'
'第一种是在创建队列的时候设置队列的 "x-message-ttl" 属性,如下:'
Map<String, Object> arguments = new HashMap<>();
// 设置消息延迟10秒;投递到该队列的消息超过10秒直接丢弃(配置死信队列的话则丢弃到死信队列)
arguments.put("x-message-ttl",10000);
// 创建队列,并指定参数
channel.queueDeclare(Normal_Queue, true, false, false, arguments);
'第二种方式针对每条消息设置TTL:'
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
// 这条消息的过期时间也被设置成了10s, 超过10秒未处理则执行到此消息后被丢弃(配置死信队列则丢弃到死信队列)
builder.expiration("10000");
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(exchangeName, routingKey, properties, "msg body".getBytes());
'重要区别的:'
第一种方式:在队列上设置TTL属性,那么一旦消息过期,就会被队列丢入死信;
第二种方式:消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,
如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间。因为消息过期也是要遵
循行先进先出原则的,比如现在生产者投递3条延迟消息,按顺序分别为60s、20s、1s;按照道理来说,
1s的消息先过期,但是在这里的情况是,先入队列的是60s,等60s的消息过期后才轮到20s的消息,会发
现20s的消息早已经过期了,那么会直接丢入死信,再然后才是1s的消息,也是直接丢入死信。
'如果不设置TTL,表示消息永远不会过期,如果将TTL设置为0,'
'则表示当前的消息可直接投递给消费者,否则该消息将会被丢弃。'
到这里,我已经基本完成了使用原生 Java RabbitMQ 客户端进行的一些基础操作,包括常用队列和交换机的使用、消息发布确认机制、消息分发等核心功能的实现。至此,本部分内容暂告一段落。
在下一篇中,我将以 Spring Boot 为基础,集成 RabbitMQ,进一步实现队列与交换机的自动化配置和消息处理。同时,我也会介绍一些常见的 RabbitMQ 插件使用方法,为构建更完整、可扩展的消息系统打下基础。