目录
[5.1 灵活的消息路由](#5.1 灵活的消息路由)
[5.2 消息可靠性](#5.2 消息可靠性)
[5.3 高可用与集群](#5.3 高可用与集群)
[5.4 灵活的消息控制](#5.4 灵活的消息控制)
[5.5 丰富的插件生态](#5.5 丰富的插件生态)
[8.1 简单模式](#8.1 简单模式)
[8.2 工作队列模式](#8.2 工作队列模式)
[8.3 发布/订阅模式](#8.3 发布/订阅模式)
[8.4 路由模式](#8.4 路由模式)
1、介绍
RabbitMQ 是一个开源的消息代理软件,通常被称为消息队列 或消息中间件 。它实现了 AMQP 高级消息队列协议标准,并提供了多种其他协议(如 MQTT、STOMP)的支持。
可以将它想象成一个高度可靠、专业的邮局。
-
应用程序(生产者) 就像寄信人,把写好的信(消息)交给邮局。
-
RabbitMQ(邮局) 负责接收、存储、路由和分发这些信件。
-
应用程序(消费者) 就像收信人,从邮局取走自己的信件。
核心思想:生产者 发送消息到 交换机 ,交换机根据规则将消息路由到一个或多个 队列 ,消费者 从队列中获取消息进行处理。生产者和消费者解耦。
RabbitMQ 本身是用 Erlang 编写的,原生实现了 AMQP 0-9-1。它的强大之处在于其插件架构。通过启用相应的插件,RabbitMQ 可以:
-
成为一个 AMQP 经纪商(这是它的本职工作)。
-
成为一个 MQTT 经纪商(通过
rabbitmq_mqtt插件)。 -
成为一个 STOMP 经纪商(通过
rabbitmq_stomp插件)。
| 特性 | AMQP | MQTT | STOMP |
|---|---|---|---|
| 核心定位 | 企业级、可靠、功能丰富 | 物联网、轻量、高效 | Web、简单、可互操作 |
| 协议格式 | 二进制 | 二进制 | 文本 |
| 消息模型 | 灵活的路由 (Exchange) | 主题(Topic)的发布/订阅 | 目的地(Destination)的发送/订阅 |
| 可靠性 | 非常强(确认、持久化、事务) | 可配置(3种 QoS) | 一般(有确认机制,但不如AMQP丰富) |
| 开销/性能 | 中等 | 极低 | 较高(文本协议,头信息大) |
| 典型场景 | 金融、电商、企业集成 | 物联网、移动推送 | Web应用、脚本语言、快速原型 |
2、核心概念
-
Producer(生产者)
-
角色:发送消息的应用程序。
-
工作 :创建一个消息,并将其发布(Push)到 RabbitMQ 的一个 Exchange 上。
-
-
Consumer(消费者)
-
角色:接收消息的应用程序。
-
工作:订阅一个队列,当有消息到达时,RabbitMQ 会将消息推送(Push)给它,或者它主动从队列中拉取(Pull)消息。
-
-
Message(消息)
-
生产者发送的数据。它不仅仅包含数据(Payload),还包含一些附加属性(Headers/Properties),如:
-
content_type: 消息体的类型(如 JSON)。 -
delivery_mode: 是否持久化(2表示持久化)。 -
reply_to: 用于指定回复队列。 -
correlation_id: 用于将 RPC 响应与请求关联起来。
-
-
-
Queue(队列)
-
本质是一个存储在 RabbitMQ 内部的消息缓冲区。消息最终会被送到队列中,等待消费者取走。
-
多个消费者可以订阅同一个队列 。默认情况下,RabbitMQ 会使用轮询的方式将队列中的消息分发给各个消费者。
-
队列可以配置属性,如是否持久化(
durable)、是否自动删除(auto_delete)等。
-
-
Exchange(交换机)
-
这是 RabbitMQ 最核心、最强大的部分。生产者从不直接发送消息到队列,而是发送到交换机。
-
交换机的职责是:根据消息的路由键(Routing Key) 和预先定义的绑定(Binding) 规则,将消息路由到一个或多个队列中。
-
如果消息没有匹配任何队列怎么办? 消息会被丢弃(或者返回给生产者,取决于配置)。
-
-
Binding(绑定)
- 连接 Exchange 和 Queue 的"桥梁"。它告诉 Exchange:"请把带有某种 Routing Key 的消息发送到这个队列来。"
-
Routing Key(路由键)
- 由生产者设置的一个消息属性,是一个字符串。Exchange 根据这个键来决定如何路由消息。
-
Virtual Host(虚拟主机)
- 类似于 Web 服务器中的虚拟主机。它提供了一个逻辑上的隔离环境,拥有自己的队列、交换机和绑定。不同的项目或团队可以使用不同的 vhost,从而实现多租户隔离。
-
Connection(连接) 与 Channel(信道):
-
Connection: 生产者/消费者与 Broker 之间的 TCP 连接。建立连接开销较大。
-
Channel: 在 Connection 内部建立的虚拟连接。几乎所有操作都在 Channel 中进行。使用 Channel 可以避免频繁创建和销毁 TCP 连接带来的巨大开销,并实现多路复用。
-
工作流程总结:
Producer -> Message -> Exchange (根据 Routing Key 和 Binding) -> Queue -> Consumer
3、工作模式
RabbitMQ 的工作模式主要围绕 生产者发送消息 到 交换机 ,交换机根据特定规则将消息路由到 队列 ,最后由 消费者 从队列中获取并处理消息这一核心流程展开。下面这个表格汇总了其主要的几种工作模式及其核心特征:
| 工作模式 | 核心角色 | 交换机类型 | 路由/分发机制 | 典型应用场景 |
|---|---|---|---|---|
| 简单模式 (Simple) | 生产者、队列、消费者 | (默认交换机) | 生产者直接发消息到指定队列,单个消费者消费。 | 一对一任务处理,如邮件单发、日志收集。 |
| 工作队列模式 (Work Queue) | 生产者、队列、多个消费者 | (默认交换机) | 一个队列被多个消费者竞争监听,每条消息只被一个消费者获取。 | 任务分发与负载均衡,如抢红包、订单处理。 |
| 发布/订阅模式 (Publish/Subscribe) | 生产者、交换机、多个队列、消费者 | fanout |
交换机将消息广播到所有绑定它的队列,每个队列的消费者都能收到同一消息。 | 广播通知,如邮件群发、广告推送、实时消息系统。 |
| 路由模式 (Routing) | 生产者、交换机、队列、消费者 | direct |
生产者发送消息时指定路由键 ,交换机将消息精确路由到绑定相同路由键的队列。 | 选择性消费,如错误日志按级别路由、特定消息通知。 |
| 主题模式 (Topic) | 生产者、交换机、队列、消费者 | topic |
使用通配符 匹配路由键,*匹配一个词,#匹配零个或多个词,实现灵活的消息路由。 |
灵活路由与多属性过滤,如物流分拣、日志按模块和级别分发。 |
| RPC模式 (Remote Procedure Call) | 客户端、服务器、回调队列 | (通常使用默认交换机) | 模拟远程过程调用,客户端发送请求消息并等待服务器返回响应消息。 | 需要同步获取结果的远程调用,如订单支付。 |
一个典型的消息从生产到消费的流程如下:
建立连接:生产者或消费者首先需要与 RabbitMQ Broker 建立一个 TCP 连接。
获取信道 :在连接之上,会创建一个信道,大部分实际操作都在信道中进行。
声明交换机与队列:生产者或消费者需要确保消息的目的地存在。这包括:
声明交换机 :指定其类型(如
fanout,direct,topic)和属性。声明队列:并设置相关属性(如是否持久化)。
建立绑定 :定义交换机和队列之间的路由规则,即 绑定 ,通常需要指定一个 绑定键 。
发布消息 :生产者将消息发送到指定的交换机 ,并提供一个路由键 。交换机根据自身的类型 和绑定规则,将消息路由到一个或多个队列中。
消费消息:消费者通过以下方式从队列中获取消息:
推模式:RabbitMQ 主动将消息推送给消费者。
拉模式:消费者主动从队列中拉取消息。
消息确认 :为确保消息被可靠处理,消费者在处理完消息后,需要向 RabbitMQ 发送一个确认,RabbitMQ 才会将消息从队列中移除。如果消费者处理失败,RabbitMQ 可以重新投递消息。
4、核心价值
-
应用解耦
-
场景: 系统A需要将数据发送给系统B和系统C。如果直接通过API调用,系统A需要知道B和C的地址,并且需要处理B或C宕机、超时等问题,耦合性极高。
-
RabbitMQ方案: 系统A只需将消息发送到RabbitMQ,系统B和C各自从RabbitMQ获取消息。系统A完全不关心谁消费消息,也不关心消费者是否在线。系统的独立性、可扩展性和容错能力大大增强。
-
-
异步通信
-
场景: 用户注册后,需要发送邮件和短信。如果同步执行,用户需要等待所有操作完成,响应时间很长。
-
RabbitMQ方案: 注册逻辑只需将"发送邮件"、"发送短信"的消息放入RabbitMQ,然后立即返回给用户。邮件和短信服务在后台异步处理。系统响应速度得到质的提升。
-
-
流量削峰
-
场景: 秒杀活动开始时,瞬时流量极高,远超后端数据库的处理能力,容易导致服务崩溃。
-
RabbitMQ方案: 将所有请求转换成消息,存入RabbitMQ队列。后端服务按照自己的能力,匀速地从队列中取出消息进行处理。消息队列像一座"水库",将瞬时的洪峰流量变得平滑,保护了后端系统的稳定性。
-
5、核心功能与特性
5.1 灵活的消息路由
通过不同类型的 Exchange(交换机),实现了极其灵活的路由策略:
-
Direct Exchange(直连) : 精确匹配。消息的 Routing Key 必须与 Binding Key 完全一致,才会路由到队列。常用于点对点通信。
-
Fanout Exchange(扇出) : 广播模式。它将消息路由到所有绑定到它的队列上,忽略 Routing Key。适用于发布/订阅场景。
-
Topic Exchange(主题) : 主题模式。通过通配符(
*匹配一个词,#匹配零个或多个词)进行模糊匹配。非常灵活,适用于根据消息的多个属性进行路由的场景。 -
Headers Exchange(头交换机): 通过消息头(Headers)而不是 Routing Key 进行匹配,使用较少。
5.2 消息可靠性
-
生产者确认(Publisher Confirm): 生产者可以确认消息是否成功送达 Broker。
-
事务:性能差,不推荐。
-
Publisher Confirm :异步回调机制。生产者开启Confirm模式,消息被Broker持久化到磁盘后,会回送一个
ack给生产者。这是生产环境保证消息可靠投递的推荐方式。
-
-
消息持久化: 可以将 Exchange、Queue 和 Message 本身设置为持久化的,即使 RabbitMQ 服务重启,消息也不会丢失。
-
队列持久化 :声明队列时设置
durable=true。 -
消息持久化 :发送消息时,将
delivery_mode属性设置为2。 -
注意:
-
仅仅设置队列持久化,重启后队列还在,但消息会丢失。必须两者都设置。
-
仅设置持久化不能100%保证消息不丢失(例如RabbitMQ刚收到消息,还未写入磁盘时就挂了)。为了更强保证,需要使用 Publisher Confirm 机制。
-
-
-
消费者确认(Consumer Acknowledgement): 消费者在成功处理消息后,向 RabbitMQ 发送一个 Ack(手动确认)。RabbitMQ 只有在收到 Ack 后,才会将消息从队列中删除。如果消费者在处理过程中失败(连接断开、未发送Ack),RabbitMQ 会将消息重新投递给另一个消费者。这保证了消息至少被消费一次。
5.3 高可用与集群
-
RabbitMQ 支持集群模式,可以将多个 Broker 组成一个逻辑 Broker,实现负载分担。
-
镜像队列: 这是RabbitMQ实现高可用的主要方式。将队列镜像到集群中的多个节点上,主节点宕机后,镜像节点会自动提升为主节点,保证服务不中断。
-
客户端需要连接集群中的多个节点,以便在主节点失效时能切换到其他节点。
5.4 灵活的消息控制
-
TTL(Time-To-Live): 可以为队列或单条消息设置过期时间,超时未消费,消息会自动过期并被丢弃(或成为死信)。
-
消息TTL: 单条消息可以设置过期时间。
-
队列TTL: 整个队列的消息统一设置过期时间。
-
过期消息会变成死信(如果队列设置了死信交换机)。
-
-
死信队列(Dead Letter Exchange, DLX): 一个消息在以下情况下会变成"死信",可以专门设置一个交换机和一个队列来接收这些"死信",用于后续分析、重试或人工干预。
-
被消费者拒绝且不重新入队。
-
消息在队列中存活时间超时。
-
队列长度达到上限。
-
-
消费端限流:
-
当消费者处理能力有限时,为避免被压垮,可以设置
prefetchCount。 -
channel.basicQos(1): 告诉Broker,在消费者未确认前,最多只推送1条消息给它。这是"公平调度"的基础。
-
5.5 丰富的插件生态
可以通过插件扩展功能,例如:
-
管理界面: 提供 Web UI,便于监控和管理 RabbitMQ。
-
STOMP、MQTT 协议支持: 使其能接入更多类型的客户端。
-
Shovel / Federation: 用于在 Broker 之间迁移或同步数据。
6、优缺点
优点:
-
成熟稳定: 经历多年生产环境考验,社区活跃,文档丰富。
-
强大的路由功能: 多种 Exchange 类型提供了极高的路由灵活性。
-
支持多种协议: 原生支持 AMQP,并通过插件支持 MQTT、STOMP 等。
-
可靠性与可用性: 通过持久化、确认机制和集群/镜像队列,提供了良好的可靠性保证。
-
管理界面友好: 自带的管理插件非常实用,便于监控和运维。
-
跨平台: 基于 Erlang 开发,具有良好的跨平台性。
缺点:
-
性能瓶颈: 相比于 Kafka、RocketMQ 等,RabbitMQ 的吞吐量相对较低,在超大规模(如日万亿级消息)场景下可能成为瓶颈。
-
Erlang 技术栈: 对于主要使用 Java/Python/Go 的团队来说,Erlang 的学习曲线可能带来二次开发和深度定制的困难。
-
消息堆积能力有限: 虽然支持持久化,但其设计初衷是快速投递,当消息大量堆积时,性能会下降,而 Kafka 是为海量消息存储设计的。
-
集群扩展稍复杂: 虽然支持集群,但其线性扩展能力不如 Kafka 那样简单直接。
三款消息队列对比:
-
选择 RabbitMQ :如果项目是企业级应用、业务系统集成 ,需要高可靠性 和灵活的路由(如将不同类型的消息精准分发给不同服务),对吞吐量的要求不是极限高度,那么RabbitMQ是绝佳选择。它的管理界面和丰富的文档也能大大降低运维和学习成本。
-
选择 RocketMQ :如果场景涉及金融交易、电商订单 等,需要严格的分布式事务 (如下单扣库存)和顺序消息保障,同时要求较高的吞吐量,RocketMQ是更针对性的解决方案。
-
选择 Kafka :如果核心业务是日志收集、网站活动追踪、IoT数据采集 或实时流处理 ,需要处理海量数据 并追求极限吞吐量,Kafka是毋庸置疑的王者。
7、典型使用场景
-
异步任务处理
- 示例: 用户上传视频后,需要转码、生成缩略图、内容分析等耗时操作。前端立即返回"上传成功",后端通过 RabbitMQ 将转码任务分发给多个 worker 异步执行。
-
应用解耦与微服务通信
- 示例: 在电商微服务架构中,"订单服务"创建订单后,发送一条消息到 RabbitMQ。"库存服务"、"用户积分服务"、"物流服务"分别订阅该消息,并执行各自的业务逻辑。服务之间不直接调用。
-
延迟任务/调度
- 示例: 利用 TTL + 死信队列实现"30分钟后检查订单是否支付,未支付则自动取消"的功能。下单时发送一条 TTL 为30分钟的消息,消息过期后进入死信队列,由专门的消费者处理超时订单。
-
数据同步/事件广播
- 示例: 使用 Fanout Exchange。当主业务数据发生变化时,发布一条消息。搜索引擎、缓存、数据分析等系统都可以接收到该消息,并同步更新自己的数据。
-
流量削峰
- 示例: 电商秒杀系统。将瞬间涌入的下单请求全部放入 RabbitMQ 队列,订单处理服务根据自身最大处理能力,从队列中匀速拉取请求进行处理,避免系统被冲垮。
8、主要工作模式代码示例
首先需在 Maven 项目中引入依赖:
XML
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.20.0</version> <!-- 请检查并使用最新版本 -->
</dependency>
8.1 简单模式
这是最基础的模式,生产者直接发送消息到队列。
- 生产者代码:
java
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
public class Producer {
// 定义队列名称
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
// 1. 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost"); // RabbitMQ服务器地址
factory.setUsername("guest"); // 用户名
factory.setPassword("guest"); // 密码
factory.setVirtualHost("/"); // 虚拟主机
// 2. 创建连接和通道
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 3. 声明队列
channel.queueDeclare(QUEUE_NAME,
false, // durable: 是否持久化
false, // exclusive: 是否独占
false, // autoDelete: 是否自动删除
null); // arguments: 其他参数
// 4. 要发送的消息
String message = "Hello World!";
// 5. 发布消息 (使用默认的AMQP默认交换机 "")
channel.basicPublish("",
QUEUE_NAME,
null,
message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
}
}
- 消费者代码:
java
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 声明队列,确保它存在
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 回调函数,处理接收到的消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
// 开始消费消息
channel.basicConsume(QUEUE_NAME,
true, // autoAck: 是否自动确认
deliverCallback,
consumerTag -> { });
}
}
8.2 工作队列模式
用于在多个消费者之间分配耗时的任务。
-
生产者代码:与简单模式类似,但通常发送更复杂的任务消息。
-
消费者代码:
java
// ... (连接和通道创建与之前类似)
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); // durable 设置为 true,使队列持久化
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 设置预取计数为1,实现公平调度
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
// 模拟耗时任务
doWork(message);
} finally {
System.out.println(" [x] Done");
// 手动发送确认消息,确保消息被正确处理
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
// 关闭自动确认 (autoAck false),处理完任务后手动确认
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
8.3 发布/订阅模式
使用 Fanout 交换机,将一条消息广播给多个消费者。
- 生产者代码:
java
// ... (创建连接和通道)
// 1. 声明一个Fanout类型的交换机
String EXCHANGE_NAME = "logs";
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 2. 发送消息到交换机,而不是特定队列。routingKey 对于 fanout 交换机会被忽略。
String message = "This is a broadcast message!";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
- 消费者代码:
java
// ... (创建连接和通道)
// 1. 声明一个Fanout类型的交换机
String EXCHANGE_NAME = "logs";
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 2. 声明一个临时队列(唯一、连接关闭时自动删除),并获取其名称
String queueName = channel.queueDeclare().getQueue();
// 3. 将临时队列绑定到交换机上
channel.queueBind(queueName, EXCHANGE_NAME, ""); // routingKey 为空
System.out.println(" [*] Waiting for broadcast messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
8.4 路由模式
使用 Direct 交换机,根据路由键将消息发送到绑定键完全匹配的队列。
- 生产者代码:
java
// ... (创建连接和通道)
// 1. 声明一个Direct类型的交换机
String EXCHANGE_NAME = "direct_logs";
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 2. 设置路由键 (例如: error, warning, info)
String severity = "error";
String message = "An error occurred!";
// 3. 发布消息到交换机,并指定路由键
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
- 消费者代码:
java
// ... (创建连接和通道)
String EXCHANGE_NAME = "direct_logs";
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String queueName = channel.queueDeclare().getQueue();
// 指定这个消费者关心的日志级别(绑定键)
String[] severities = {"error", "warning"}; // 可以绑定多个键
for (String severity : severities) {
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
System.out.println(" [*] Waiting for direct messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
String routingKey = delivery.getEnvelope().getRoutingKey();
System.out.println(" [x] Received '" + routingKey + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });