RabbitMQ架构与集群模式详解
前言
假设你维护了两个服务 A 和 B。A 服务负责转发用户请求到 B 服务,B 服务是个算法服务,GPU 资源有限。当请求量大到 B 服务处理不过来的时候,希望能优先处理会员用户的请求。怎么实现?答案是 RabbitMQ 的优先级队列。本文将带你深入了解 RabbitMQ 的核心概念和集群架构。
🏠个人主页:你的主页
文章目录
一、RabbitMQ核心概念
1.1 Queue(队列)
消息队列本质上就是一个类似链表的独立进程,链表里的每个节点是一条消息。
生产者 → [消息1] → [消息2] → [消息3] → 消费者
↑ 这就是 Queue
Queue 的作用是什么?削峰填谷。
在流量高峰时先暂存数据,再慢慢消费,保护下游服务不被打垮。
打个比方:Queue 就像银行的叫号系统,客户来了先取号排队,柜员按顺序处理,不会因为人多就手忙脚乱。
但消息也分很多种类,比如订单消息和用户消息是两类。为了更好地管理不同种类的数据,RabbitMQ 支持创建多个队列:
RabbitMQ
├── order-queue → 订单消息
├── user-queue → 用户消息
└── payment-queue → 支付消息
每个 Queue 都是独立的进程,某个进程挂了,不影响其他进程正常工作。
1.2 Exchange(交换器)
有了多个队列,新的问题来了:
- 有些生产者想把消息发到一个 Queue
- 有些想发到多个 Queue
- 有些想广播给所有 Queue
怎么实现这种灵活的路由?答案是 Exchange(交换器)。
Exchange 是消息的路由中心,生产者不直接发消息给 Queue,而是发给 Exchange,由 Exchange 根据规则分发到对应的 Queue。
生产者 → Exchange → Queue-1
↘ → Queue-2
↘ → Queue-3
Exchange 和 Queue 之间通过 Binding Key(绑定键) 建立绑定关系,类似正则表达式,声明"什么样的消息发到哪个队列"。
四种 Exchange 类型:
| 类型 | 路由规则 | 场景 |
|---|---|---|
| Direct | 精确匹配 Routing Key | 点对点,一对一 |
| Fanout | 广播给所有绑定的队列 | 广播通知 |
| Topic | 通配符匹配(* 和 #) |
灵活路由 |
| Headers | 根据消息头匹配 | 复杂路由(少用) |
举个例子:
# Topic Exchange 示例
Routing Key: order.created → 匹配 order.* → 发到订单队列
Routing Key: order.paid → 匹配 order.* → 发到订单队列
Routing Key: user.login → 匹配 user.* → 发到用户队列
1.3 Broker
一台服务器上的 RabbitMQ 实例,就是一个 Broker。
Broker(RabbitMQ 实例)
├── Exchange-1
├── Exchange-2
├── Queue-1
├── Queue-2
└── Queue-3
Broker 里维护的路由规则和绑定关系,统称为元数据(Metadata)。
1.4 整体架构图
┌─────────────────────────────────────────────────────────┐
│ Broker │
│ ┌──────────┐ │
│ │ Exchange │──bindingKey──→ Queue-1 ──→ Consumer-1 │
│ │ │──bindingKey──→ Queue-2 ──→ Consumer-2 │
│ └──────────┘──bindingKey──→ Queue-3 ──→ Consumer-3 │
│ ↑ │
└───────│─────────────────────────────────────────────────┘
│
Producer
二、RabbitMQ的特色功能
RabbitMQ 功能非常丰富,你能想到的 MQ 功能它基本都实现了。
2.1 延迟队列
场景:用户下单后 30 分钟未支付,自动取消订单。
RabbitMQ 通过死信队列 + TTL 或者延迟插件实现延迟消息。
订单创建 → 延迟队列(30分钟后投递) → 检查支付状态 → 未支付则取消
2.2 死信队列
消息在以下情况会变成"死信":
- 消息被拒绝(reject/nack)且不重新入队
- 消息过期(TTL 到期)
- 队列达到最大长度
死信会被转发到专门的死信队列(DLX),方便后续排查和处理。
2.3 优先级队列(重点)
这是 RabbitMQ 的特色功能,也是开头问题的解决方案。
原理 :生产者发送消息时,可以为消息标记优先级(0-255),消费者总是优先消费优先级高的消息。
java
// 声明优先级队列(最大优先级为 10)
Map<String, Object> args = new HashMap<>();
args.put("x-max-priority", 10);
channel.queueDeclare("priority-queue", true, false, false, args);
// 发送高优先级消息(会员用户)
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.priority(9) // 高优先级
.build();
channel.basicPublish("", "priority-queue", props, "VIP用户请求".getBytes());
// 发送低优先级消息(普通用户)
AMQP.BasicProperties props2 = new AMQP.BasicProperties.Builder()
.priority(1) // 低优先级
.build();
channel.basicPublish("", "priority-queue", props2, "普通用户请求".getBytes());
回到开头的场景:
用户请求 → A服务(根据会员等级设置优先级) → RabbitMQ → B服务(GPU算法服务)
↓
优先消费高优消息
- 会员用户的请求:优先级设为 9
- 普通用户的请求:优先级设为 1
- B 服务永远优先处理会员请求,处理完后再处理普通请求
实际应用:现在到处都是 AI,恨不得把一块 GPU 掰成 10 块用。比如某聊天 AI,当服务遭到大量访问时,免费用户会感觉很慢甚至报错,但会员用户依旧响应丝滑。背后很可能就是优先级队列在起作用。
三、单节点的问题
虽然 RabbitMQ 功能很丰富,但它的基础架构就是个单实例节点,存在明显问题:
| 问题 | 说明 |
|---|---|
| 单点故障 | Broker 挂了,整个服务不可用 |
| 性能瓶颈 | 单节点处理能力有限 |
| 无法扩展 | 流量增长时没法水平扩容 |
怎么解决?上集群。
四、普通集群模式
4.1 架构设计
多个服务器各部署一个 RabbitMQ 实例,通过 RabbitMQ 提供的命令组成集群。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Broker-1 │ │ Broker-2 │ │ Broker-3 │
│ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │
│ │ Q1 │ │ │ │ Q2 │ │ │ │ Q3 │ │
│ └───────┘ │ │ └───────┘ │ │ └───────┘ │
│ Exchange │←──→│ Exchange │←──→│ Exchange │
│ (元数据同步) │ │ (元数据同步) │ │ (元数据同步) │
└─────────────┘ └─────────────┘ └─────────────┘
关键特点:
- 每个 Broker 都是完整功能的 RabbitMQ 实例,都能进行读写
- Broker 之间会互相同步 Exchange 里的元数据
- 但不会同步 Queue 里的数据
4.2 读写流程
写操作:
生产者 → Broker-1 → Q1(数据只存在 Broker-1)
↓
元数据同步到 Broker-2、Broker-3
Q1 里的消息数据不会同步给其他 Broker,但 Exchange 的元数据会同步。
读操作:
情况一:消费者访问 Q1 所在的 Broker-1
消费者 → Broker-1 → Q1 → 直接返回数据 ✅
情况二:消费者访问 Broker-2(但 Q1 在 Broker-1)
消费者 → Broker-2 → 查元数据发现 Q1 在 Broker-1
↓
从 Broker-1 读取数据 → 返回给消费者
4.3 优缺点
| 优点 | 缺点 |
|---|---|
| 提升整体吞吐量 | 单个 Queue 的读写能力没提升 |
| 支持水平扩展 | 每个 Broker 仍有单点问题 |
| 部署简单 | Queue 所在 Broker 挂了,该 Queue 就不可用 |
结论 :普通集群模式提升了扩展性,但没有解决高可用问题。
五、镜像队列集群模式
5.1 设计思路
既然单个 Queue 有单点问题,那就给它加几个副本。
打个比方:就像你手机里存了很多重要照片,为了防止手机丢了照片没了,你会把照片备份到云盘。镜像队列就是给 Queue 做备份。
5.2 架构设计
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Broker-1 │ │ Broker-2 │ │ Broker-3 │
│ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │
│ │Q1(主) │──────→│ Q1(镜像)│──────→│ Q1(镜像)│ │
│ └───────┘ │ │ └───────┘ │ │ └───────┘ │
│ │ │ ┌───────┐ │ │ │
│ │ │ │Q2(主) │ │ │ │
│ │ │ └───────┘ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
- 主队列(Master):负责读写数据
- 镜像队列(Mirror):负责同步复制主队列数据
- 主队列所在的 Broker 挂了,镜像队列可以顶上成为新的主队列
5.3 读写流程
写操作:
生产者 → Broker-1 → Q1(主)
↓ 同步
Q1(镜像)@Broker-2、Q1(镜像)@Broker-3
数据写入主队列后,会同步到所有镜像队列。
读操作:
情况一:消费者访问主队列所在的 Broker
消费者 → Broker-1 → Q1(主) → 直接返回数据 ✅
情况二:消费者访问其他 Broker
消费者 → Broker-2 → 从 Broker-1 的 Q1(主) 读取 → 返回数据
注意:即使 Broker-2 上有 Q1 的镜像,读操作也是从主队列读取,镜像只用于故障转移。
5.4 故障转移
Broker-1 挂了!
↓
Q1(镜像)@Broker-2 自动升级为 Q1(主)
↓
服务继续可用 ✅
5.5 优缺点
| 优点 | 缺点 |
|---|---|
| 实现高可用 | 牺牲吞吐量(数据要同步多份) |
| 自动故障转移 | 网络带宽消耗大 |
| 数据不丢失 | 同步延迟可能导致数据不一致 |
结论 :镜像队列模式通过牺牲吞吐量换取高可用。
六、Quorum队列模式
6.1 镜像队列的问题
RabbitMQ 基于 Erlang 语言开发。Erlang 是个很特别的语言,自带虚拟机和分布式通信框架。RabbitMQ 通过这个框架在 Broker 间同步元数据。
但有个问题:如果 Broker 间通信断开(网络分区),可能出现多个节点都认为自己是主节点 的情况,导致数据不一致。这就是所谓的脑裂问题。
打个比方:公司有三个办公室,平时通过内网通信。突然网络断了,每个办公室都以为自己是总部,各自做决策,等网络恢复后发现决策冲突了。
6.2 Quorum 队列的解决方案
RabbitMQ 3.8 版本引入了 Quorum 队列 ,使用 Raft 一致性算法来解决脑裂问题。
Raft 算法核心思想:
-
集群中只有一个 Leader,其他都是 Follower
-
任何写操作必须经过多数节点(Quorum)确认才算成功
-
Leader 挂了,通过选举产生新 Leader
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Broker-1 │ │ Broker-2 │ │ Broker-3 │
│ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │
│ │Q1 │ │ │ │Q1 │ │ │ │Q1 │ │
│ │(Leader)│←────→│ (Follower)│←────→│ (Follower)│ │
│ └───────┘ │ │ └───────┘ │ │ └───────┘ │
└─────────────┘ └─────────────┘ └─────────────┘
↑
Raft 选举
写入流程:
1. 生产者发送消息到 Leader
2. Leader 将消息复制给 Follower
3. 超过半数节点确认后,Leader 返回成功
4. 如果 Leader 挂了,剩余节点选举新 Leader
6.3 为什么 Quorum 能解决脑裂
假设 3 个节点,网络分区后变成 1 + 2 两个分区:
- 1 个节点的分区:无法获得多数票(需要 2 票),不能选出 Leader,停止服务
- 2 个节点的分区:可以获得多数票,选出 Leader,继续服务
这样就保证了任何时刻最多只有一个 Leader,避免了脑裂。
6.4 Quorum 队列 vs 镜像队列
| 特性 | 镜像队列 | Quorum 队列 |
|---|---|---|
| 一致性算法 | 无(简单同步) | Raft |
| 脑裂处理 | 可能脑裂 | 不会脑裂 |
| 数据一致性 | 最终一致 | 强一致 |
| 性能 | 较高 | 略低(需要多数确认) |
| 推荐场景 | 对一致性要求不高 | 对数据一致性要求高 |
七、集群模式对比与选型
7.1 三种模式对比
| 模式 | 高可用 | 高吞吐 | 数据一致性 | 复杂度 |
|---|---|---|---|---|
| 普通集群 | ❌ | ✅ | - | 低 |
| 镜像队列 | ✅ | ❌ | 最终一致 | 中 |
| Quorum队列 | ✅ | ❌ | 强一致 | 中 |
7.2 选型建议
选普通集群:
- 对高可用要求不高
- 追求高吞吐量
- 消息丢失可以接受
选镜像队列:
- 需要高可用
- 对数据一致性要求不是特别严格
- RabbitMQ 版本低于 3.8
选 Quorum 队列:
- 需要高可用
- 对数据一致性要求高
- RabbitMQ 版本 3.8+
- 金融、支付等关键业务
7.3 RabbitMQ vs Kafka vs RocketMQ
| 特性 | RabbitMQ | Kafka | RocketMQ |
|---|---|---|---|
| 定位 | 企业级消息中间件 | 大数据流处理 | 业务消息中间件 |
| 协议 | AMQP | 自定义 | 自定义 |
| 优先级队列 | ✅ 原生支持 | ❌ | ❌ |
| 延迟队列 | ✅ | ❌ | ✅ |
| 死信队列 | ✅ | ❌ | ✅ |
| 事务消息 | ✅ | ⚠️ 仅发送端 | ✅ |
| 吞吐量 | 万级 | 十万级 | 十万级 |
| 适用场景 | 复杂路由、企业集成 | 日志、大数据 | 电商、金融业务 |
八、总结
本文介绍了 RabbitMQ 的核心概念和集群架构:
| 概念 | 说明 |
|---|---|
| Queue | 存储消息的队列,独立进程 |
| Exchange | 消息路由中心,根据规则分发消息 |
| Broker | RabbitMQ 实例 |
| 元数据 | Exchange 里的路由规则和绑定关系 |
三种集群模式:
| 模式 | 核心思想 |
|---|---|
| 普通集群 | 同步元数据,不同步队列数据,提升吞吐 |
| 镜像队列 | 队列数据多副本,牺牲吞吐换高可用 |
| Quorum队列 | Raft 算法,强一致性,解决脑裂 |
核心结论 :做架构,做到最后,都是在做折中。普通集群牺牲高可用换吞吐,镜像队列牺牲吞吐换高可用,没有完美的方案,只有适合的场景。
热门专栏推荐
- Agent小册
- Java基础合集
- Python基础合集
- Go基础合集
- 大数据合集
- 前端小册
- 数据库合集
- Redis 合集
- Spring 全家桶
- 微服务全家桶
- 数据结构与算法合集
- 设计模式小册
- 消息队列合集
等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟