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 实战建议

说了这么多,到底怎么选?我个人的经验是:

  1. 优先用 Direct:80% 的场景它都够用,简单可靠
  2. 需要通配符时用 Topic:比如按地域、服务类型分发
  3. 广播场景用 Fanout:别犹豫,它就是干这个的
  4. Headers 慎用:性能开销大,除非路由规则真的复杂到没法用路由键表达

避坑指南:我曾经在一个项目里混用了 Direct 和 Topic,结果运维同学排查问题时差点崩溃。建议一个交换机只做一件事,命名规范要清晰,比如 exchange.direct.logsexchange.topic.events

最后说一句:交换机类型选对了,消息路由就成功了一半。另一半,是做好监控和容错。嗯,这个我们后面章节再聊。