RabbitMQ 的整体架构是怎样的?
结论:RabbitMQ 是 AMQP 0-9-1 的实现,消息不直接发给队列,而是先到交换机(Exchange),由绑定(Binding)和路由键(Routing Key)决定投递到哪些队列。Broker 内部用虚拟主机(vhost)做租户隔离,一条 TCP 连接(Connection)上可复用多个信道(Channel),每个队列由独立的 Erlang 进程承载。
一、消息流转
Producer
│ basicPublish(exchange, routingKey, body)
▼
┌──────────────── Broker ──────────────────────────────┐
│ vhost: /order │
│ │
│ Exchange "order.ex" ──binding(order.#)──> Queue A │──> Consumer 1
│ (topic) ──binding(order.pay)─> Queue B │──> Consumer 2
│ │
│ 每个 Queue 是独立的 Erlang 进程,消息在进程内排队 │
└──────────────────────────────────────────────────────┘
四个交换机类型决定了"一条消息能扩散到几个队列":
| 类型 | 路由规则 | 典型用途 |
|---|---|---|
| direct | routing key 完全相等 | 点对点任务分发 |
| fanout | 忽略 routing key,广播到所有绑定队列 | 缓存刷新、配置通知 |
| topic | 按 *(一个词)与 #(零到多个词)通配 | 按业务维度订阅,如 order.*.created |
| headers | 按消息头键值匹配 | 多条件路由,性能较差,少用 |
默认交换机(名字为空字符串)是 direct 类型,routing key 直接写队列名,这也是"不声明交换机也能发消息"的原因。
二、核心组件
| 组件 | 作用 | 注意点 |
|---|---|---|
| Connection | 一条 TCP 长连接 | 创建成本高,进程内应复用 |
| Channel | 连接上的虚拟信道 | 非线程安全,建议每线程一个 |
| vhost | 逻辑隔离单元 | 权限、交换机、队列都按 vhost 隔离 |
| Exchange | 路由规则 | 声明为 durable 才能重启存活 |
| Queue | 消息存储 | 单队列单进程,吞吐有上限 |
| Binding | 交换机与队列的关联 | 绑定关系存元数据,集群内全量复制 |
因为每个队列是一个 Erlang 进程、内部近似单线程处理,单队列的吞吐存在天花板(万级每秒)。要提升吞吐只能增加队列数并让消费者分散,而不是给某个队列加消费者。
三、声明与绑定的代码
ConnectionFactory f = new ConnectionFactory();
f.setHost("10.0.0.21");
f.setVirtualHost("/order");
f.setUsername("app");
f.setPassword("***");
try (Connection conn = f.newConnection(); Channel ch = conn.createChannel()) {
// durable=true 让交换机/队列在重启后仍然存在
ch.exchangeDeclare("order.ex", BuiltinExchangeType.TOPIC, true);
ch.queueDeclare("order.pay.q", true, false, false,
Map.of("x-queue-type", "quorum")); // 仲裁队列,见高可用一章
ch.queueBind("order.pay.q", "order.ex", "order.pay.*");
}
# 观察拓扑与积压
rabbitmqctl list_exchanges name type durable
rabbitmqctl list_queues -p /order name messages consumers
rabbitmqctl list_bindings -p /order
四、常见追问
- Channel 为什么不共享? 信道上的 confirm、事务、prefetch 都是连接级状态,多线程共用一个 Channel 会出现帧交错与确认错乱,官方建议每线程一个。
- vhost 和数据库的库是一回事吗? 类似但更彻底:不同 vhost 的交换机、队列、权限、策略互不可见,多租户或环境隔离常用它。
- 消息在队列里是什么顺序? FIFO,但仅限于单队列;多个队列之间没有全局顺序,需要顺序时只能用单队列单消费者。
- 交换机类型能改吗? 不能。类型是声明时确定的属性,修改需要删除重建并重新绑定,生产环境应通过"新交换机 + 迁移绑定"的方式灰度。
小结
- RabbitMQ 的路由核心是"交换机 + 绑定 + routing key",队列只是消息的落脚点。
- vhost 做隔离、Connection 复用、Channel 每线程一个,是使用层面的三条基本纪律。
- 队列是独立 Erlang 进程,单队列吞吐有限,扩展手段是加队列而不是加消费者。
- 声明时必须显式设置 durable,否则重启后拓扑消失,消息自然也就没了。