RabbitMQ交换机和路由
RabbitMQ 中生产者并不直接发送消息到 Queue,而是发送到 Exchange。Exchange 根据自己的类型、routing key、binding key 决定消息应该进入哪些队列。
为什么路由设计很重要
Exchange 是 RabbitMQ 的“分发器”。同一条消息进入哪个队列,取决于交换机类型和绑定规则。如果路由键设计混乱,短期能跑,后期会出现三个问题:生产者不知道该发什么 routing key,消费者不知道自己为什么收到某类消息,排查时也很难判断消息到底应该进哪个队列。
路由匹配原理
flowchart TD
A["Producer 发布消息"] --> B["Exchange 收到 routing key"]
B --> C["读取绑定关系 Binding"]
C --> D{"交换机类型"}
D -- "direct" --> E["完全匹配 binding key"]
D -- "fanout" --> F["忽略 routing key<br/>投递所有绑定队列"]
D -- "topic" --> G["按 * 和 # 通配符匹配"]
E --> H["写入匹配队列"]
F --> H
G --> Hdirect 交换机
direct 交换机要求 routing key 和 binding key 完全一致。
flowchart TD
P["Producer: order.created"] --> E["Direct Exchange"]
E -->|order.created| Q1["创建订单队列"]
E -->|order.paid| Q2["支付订单队列"]适合明确的一对一路由。例如 order.created 只进入创建订单队列,order.paid 只进入支付订单队列。
fanout 交换机
fanout 交换机会忽略 routing key,把消息广播给所有绑定队列。
flowchart TD
P["Producer"] --> E["Fanout Exchange"]
E --> Q1["短信队列"]
E --> Q2["邮件队列"]
E --> Q3["站内信队列"]适合广播事件,例如“系统配置刷新”“用户注册成功通知多个系统”。
topic 交换机
topic 交换机使用通配符匹配 routing key。
*匹配一个单词。#匹配零个或多个单词。
flowchart TD
P["Producer: order.pay.success"] --> E["Topic Exchange"]
E -->|order.*.success| Q1["订单成功事件队列"]
E -->|order.#| Q2["所有订单事件队列"]
E -->|*.pay.*| Q3["支付相关队列"]适合多维度订阅。例如订单、支付、物流都可以按业务域、动作、结果组合 routing key。
headers 交换机
headers 交换机根据消息头匹配,不依赖 routing key。它灵活但性能和可读性不如 direct/topic,业务中较少使用。
路由设计建议
- routing key 使用业务语义,不要使用无意义编号。
- 单词之间用点分隔,例如
order.pay.success。 - direct 适合明确路由,topic 适合分类订阅,fanout 适合广播。
- 不要让一个 Exchange 承担所有业务域,容易导致绑定关系混乱。
- 重要消息要配合发布确认和死信队列。
如果不会设计 routing key,常见后果是:一个队列收到太多不相关消息,只能在消费者代码里再次判断;或者一个业务事件要复制发送多次,生产者和消费者耦合越来越重。好的 routing key 应该让“消息属于哪个业务域、发生了什么动作、结果是什么”一眼可见。
和 RocketMQ Topic 的区别
RocketMQ 通常是生产者发送到 Topic,消费者订阅 Topic。RabbitMQ 则多了一层 Exchange 路由。
flowchart TD
A[RocketMQ] --> B[Producer -> Topic -> ConsumerGroup]
C[RabbitMQ] --> D[Producer -> Exchange -> Queue -> Consumer]RocketMQ 的模型更适合高吞吐主题订阅;RabbitMQ 的 Exchange 模型在复杂路由上更直观。
配置 Demo:声明交换机、队列和绑定
下面用 Spring AMQP 声明一个 topic 交换机和两个队列。
@Configuration
public class RabbitConfig {
@Bean
TopicExchange orderExchange() {
return new TopicExchange("order.exchange", true, false);
}
@Bean
Queue paidQueue() {
return QueueBuilder.durable("order.paid.queue").build();
}
@Bean
Queue allOrderQueue() {
return QueueBuilder.durable("order.all.queue").build();
}
@Bean
Binding paidBinding() {
return BindingBuilder.bind(paidQueue())
.to(orderExchange())
.with("order.pay.success");
}
@Bean
Binding allOrderBinding() {
return BindingBuilder.bind(allOrderQueue())
.to(orderExchange())
.with("order.#");
}
}order.pay.success 只进入支付成功队列,order.# 会匹配所有订单事件。
常见风险
| 风险 | 为什么会发生 | 处理方式 |
|---|---|---|
| 消息无法路由 | routing key 和 binding key 不匹配 | 开启 mandatory 和 return callback |
| fanout 被滥用 | 广播到所有队列,消费者收到无关消息 | 只用于确实需要广播的事件 |
| topic 规则过宽 | # 匹配太多消息 | 优先使用明确的业务前缀 |
| 一个 Exchange 混所有业务 | 绑定关系越来越乱 | 按业务域拆分 Exchange |
| 队列未持久化 | Broker 重启后队列丢失 | 声明 durable queue/exchange |
