3、RabbitMQ交换机:Direct、Topic、Fanout、Headers四种交换机类型详解与实战
消息队列里,交换机是个核心角色。说白了,生产者把消息扔给交换机,交换机再根据规则分发给队列。RabbitMQ提供了四种交换机类型,我刚开始接触时也觉得有点绕,但用多了就会发现,每种都有它最擅长的场景。
3.1 交换机的基本概念
先理清一个概念:交换机本身不存储消息。它就是个路由器,收到消息后,根据绑定规则(Binding)和路由键(Routing Key),决定把消息投递到哪个队列。
我个人习惯把交换机想象成快递分拣中心。消息就是包裹,路由键就是地址标签。不同的交换机类型,就是不同的分拣策略。
3.2 Direct Exchange(直连交换机)
这是最直观的一种。消息的路由键,必须和队列绑定的路由键完全匹配,才能投递过去。
工作原理:
- 队列绑定一个路由键,比如 "error"
- 生产者发送消息时指定路由键 "error"
- 交换机精确匹配,把消息投递到该队列
适用场景:日志分级处理。比如 error 级别的日志走一个队列,warning 走另一个队列。
// 生产者示例
channel.BasicPublish(
exchange: "direct_logs",
routingKey: "error",
basicProperties: null,
body: Encoding.UTF8.GetBytes("这是一条错误日志")
);
// 消费者绑定
channel.QueueBind(
queue: "error_queue",
exchange: "direct_logs",
routingKey: "error"
);
我在项目中遇到过一个问题:有人把路由键写成了 "Error"(大写E),结果消息死活收不到。嗯,这里要注意,路由键是大小写敏感的。
3.3 Topic Exchange(主题交换机)
Topic 比 Direct 灵活得多。它支持通配符匹配:
*匹配一个单词#匹配零个或多个单词
你想想看,如果系统里有几十种日志类型,用 Direct 就得绑几十个路由键,多累啊。Topic 就优雅多了。
// 队列绑定通配符
channel.QueueBind(
queue: "critical_queue",
exchange: "topic_logs",
routingKey: "*.critical.*"
);
// 发送消息
channel.BasicPublish(
exchange: "topic_logs",
routingKey: "auth.critical.login",
body: ...
);
个人经验:我建议用点号分隔的层级结构来设计路由键,比如 "服务.级别.操作"。这样后期扩展起来特别方便。
我曾经踩过一个坑:用 # 匹配时,以为它只匹配一个层级,结果它把整个树都匹配了。后来才明白,# 是贪婪匹配,用的时候要小心。
3.4 Fanout Exchange(扇出交换机)
Fanout 最简单粗暴。它不关心路由键,把消息广播到所有绑定的队列。
说白了,就是群发。你发一条消息,所有订阅的队列都能收到。
// 生产者 - 路由键会被忽略
channel.BasicPublish(
exchange: "fanout_notifications",
routingKey: "", // 随便写什么都会被忽略
body: Encoding.UTF8.GetBytes("系统维护通知")
);
典型场景:
- 系统公告广播
- 配置更新通知
- 缓存刷新
注意:Fanout 的性能开销和绑定的队列数量成正比。我曾经在一个项目里绑了上百个队列,结果消息延迟飙升。后来加了交换机集群才解决。
3.5 Headers Exchange(头交换机)
这个用得比较少,但特定场景下很强大。它不依赖路由键,而是根据消息的 headers 属性来匹配。
匹配规则有两种:
x-match = all:所有 header 都必须匹配(AND 逻辑)x-match = any:只要有一个 header 匹配即可(OR 逻辑)
// 绑定队列时指定 headers
var args = new Dictionary<string, object>
{
{ "x-match", "all" },
{ "format", "pdf" },
{ "type", "report" }
};
channel.QueueBind(
queue: "pdf_report_queue",
exchange: "headers_exchange",
routingKey: "",
arguments: args
);
// 发送消息时设置 headers
var props = channel.CreateBasicProperties();
props.Headers = new Dictionary<string, object>
{
{ "format", "pdf" },
{ "type", "report" }
};
channel.BasicPublish(
exchange: "headers_exchange",
routingKey: "",
basicProperties: props,
body: ...
);
什么时候用 Headers? 当路由规则复杂到用路由键表达起来很别扭时。比如需要根据多个维度(格式、类型、优先级)组合匹配。
3.6 四种交换机对比
| 类型 | 匹配依据 | 路由键 | 性能 | 典型场景 |
|---|---|---|---|---|
| Direct | 精确匹配 | 必须 | 高 | 日志分级、命令分发 |
| Topic | 通配符匹配 | 必须 | 中 | 事件路由、监控告警 |
| Fanout | 广播 | 忽略 | 高 | 通知广播、缓存刷新 |
| Headers | 多属性匹配 | 忽略 | 低 | 复杂路由规则 |
3.7 实战建议
说了这么多,到底怎么选?我个人的经验是:
- 优先用 Direct:80% 的场景它都够用,简单可靠
- 需要通配符时用 Topic:比如按地域、服务类型分发
- 广播场景用 Fanout:别犹豫,它就是干这个的
- Headers 慎用:性能开销大,除非路由规则真的复杂到没法用路由键表达
避坑指南:我曾经在一个项目里混用了 Direct 和 Topic,结果运维同学排查问题时差点崩溃。建议一个交换机只做一件事,命名规范要清晰,比如 exchange.direct.logs、exchange.topic.events。
最后说一句:交换机类型选对了,消息路由就成功了一半。另一半,是做好监控和容错。嗯,这个我们后面章节再聊。