4、RabbitMQ 交换机模式:原理与代码实现

消息队列的核心,说白了就是「生产者发消息,消费者收消息」。但消息怎么发、发给谁,这里面的门道可不少。RabbitMQ 用交换机(Exchange)来解决这个问题。我个人习惯把交换机想象成一个「智能分拣员」——它根据你设定的规则,把消息投递到对应的队列里。

今天咱们就聊聊四种交换机模式:Direct、Topic、Fanout、Headers。每种模式我都踩过坑,咱们边聊边看代码。

4.1 交换机的基础概念

在动手写代码之前,先搞清楚几个关键角色:

  • 生产者(Producer):发消息的人
  • 交换机(Exchange):消息的
  • 队列(Queue):消息的临时仓库
  • 消费者(Consumer):取消息的人
  • 路由键(Routing Key):消息的「地址标签」
  • 绑定键(Binding Key):队列的「收件规则」

嗯,这里要注意:交换机和队列之间通过绑定键建立关系。消息到了交换机,交换机会拿消息的路由键去匹配队列的绑定键。匹配上了,消息就进队列;匹配不上,消息就丢了。

我曾经踩过的坑:刚用 RabbitMQ 时,我以为消息发到交换机就万事大吉了。结果消费者一直收不到消息,排查了半天才发现——交换机根本没绑定队列。记住:交换机不存储消息,它只负责转发。没有队列绑定,消息就石沉大海了。

4.2 Direct Exchange(直连交换机)

Direct Exchange 是最简单、最直观的模式。它的规则就一句话:路由键必须完全匹配绑定键。

举个例子:你给「order.create」这个路由键发消息,那只有绑定了「order.create」这个绑定键的队列才能收到。多一个字符、少一个字符都不行。

我在项目中用 Direct Exchange 处理过支付回调通知。不同支付渠道(、支付宝、银联)各自绑定不同的路由键,互不干扰。

代码实现

// 声明交换机
channel.exchangeDeclare("direct.exchange", BuiltinExchangeType.DIRECT);

// 声明队列
channel.queueDeclare("queue.order", true, false, false, null);
channel.queueDeclare("queue.payment", true, false, false, null);

// 绑定队列到交换机,指定绑定键
channel.queueBind("queue.order", "direct.exchange", "order.create");
channel.queueBind("queue.payment", "direct.exchange", "payment.success");

// 生产者发送消息
String message = "新订单已创建";
channel.basicPublish("direct.exchange", "order.create", null, message.getBytes());
// 这条消息只会进入 queue.order

// 消费者消费
channel.basicConsume("queue.order", true, (consumerTag, delivery) -> {
    String msg = new String(delivery.getBody(), "UTF-8");
    System.out.println("收到订单消息: " + msg);
}, consumerTag -> {});
小技巧:Direct Exchange 的绑定键建议用「点号分隔」的命名规范,比如「order.create」「user.register」。这样后期切换到 Topic Exchange 时,改起来也方便。

4.3 Topic Exchange(主题交换机)

Topic Exchange 比 Direct 灵活得多。它支持通配符匹配:

  • * 匹配一个单词
  • # 匹配零个或多个单词

你想想看,如果系统里有几十种消息类型,用 Direct 你得写几十个绑定。但用 Topic,一个「order.#」就能搞定所有订单相关的消息。

我记得有一次做日志收集系统,日志类型有 error、warn、info,模块有 user、order、payment。用 Topic Exchange 一个「*.error」就能捕获所有模块的错误日志,简直不要太爽。

代码实现

// 声明 Topic 交换机
channel.exchangeDeclare("topic.exchange", BuiltinExchangeType.TOPIC);

// 声明队列
channel.queueDeclare("queue.all.orders", true, false, false, null);
channel.queueDeclare("queue.payment.only", true, false, false, null);

// 绑定:queue.all.orders 接收所有订单相关消息
channel.queueBind("queue.all.orders", "topic.exchange", "order.#");

// 绑定:queue.payment.only 只接收支付成功或失败的消息
channel.queueBind("queue.payment.only", "topic.exchange", "payment.*");

// 发送消息
channel.basicPublish("topic.exchange", "order.created", null, "订单创建".getBytes());
channel.basicPublish("topic.exchange", "order.paid", null, "订单支付".getBytes());
channel.basicPublish("topic.exchange", "payment.success", null, "支付成功".getBytes());

// queue.all.orders 会收到前两条
// queue.payment.only 只会收到第三条
注意:Topic Exchange 的绑定键不能随意用「.」和「#」。我曾经在生产环境把绑定键写成了「order#」,结果通配符没生效,消息全丢了。正确的写法是「order.#」——点号不能省。

4.4 Fanout Exchange(扇出交换机)

Fanout Exchange 是最「暴力」的模式。它忽略路由键,把消息广播到所有绑定的队列。

说白了就是:你喊一嗓子,所有人都能听见。管你绑定了什么键,只要绑定了这个交换机,消息就照单全收。

我在项目中用 Fanout 做过配置中心。配置更新时,所有服务实例都得知道。用 Fanout 广播一条消息,所有队列都收到,简单粗暴有效。

代码实现

// 声明 Fanout 交换机
channel.exchangeDeclare("fanout.exchange", BuiltinExchangeType.FANOUT);

// 声明三个队列
channel.queueDeclare("queue.service.a", true, false, false, null);
channel.queueDeclare("queue.service.b", true, false, false, null);
channel.queueDeclare("queue.service.c", true, false, false, null);

// 绑定队列到交换机(Fanout 不需要绑定键)
channel.queueBind("queue.service.a", "fanout.exchange", "");
channel.queueBind("queue.service.b", "fanout.exchange", "");
channel.queueBind("queue.service.c", "fanout.exchange", "");

// 发送消息,路由键随便写,反正没用
channel.basicPublish("fanout.exchange", "", null, "配置已更新".getBytes());
// 三个队列都会收到这条消息
避坑指南:Fanout 虽然方便,但别滥用。我曾经在一个项目里,所有消息都用 Fanout 广播,结果消费者处理不过来,消息堆积严重。后来改成 Topic 按需订阅,性能提升了 3 倍。

4.5 Headers Exchange(头交换机)

Headers Exchange 是最「另类」的模式。它不看路由键,而是看消息的头部属性。

匹配规则有两种:

  • x-match = all:所有头部键值对都必须匹配(AND 逻辑)
  • x-match = any:只要有一个头部键值对匹配就行(OR 逻辑)

说实话,Headers Exchange 我用得不多。但在某些场景下它确实很香——比如消息的过滤条件特别复杂,用路由键表达不了的时候。

代码实现

// 声明 Headers 交换机
channel.exchangeDeclare("headers.exchange", BuiltinExchangeType.HEADERS);

// 声明队列
channel.queueDeclare("queue.high.priority", true, false, false, null);

// 设置绑定参数:要求消息头部必须包含 level=high 和 type=error
Map<String, Object> bindArgs = new HashMap<>();
bindArgs.put("x-match", "all");  // 必须全部匹配
bindArgs.put("level", "high");
bindArgs.put("type", "error");

channel.queueBind("queue.high.priority", "headers.exchange", "", bindArgs);

// 发送消息:设置头部属性
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
    .headers(Map.of("level", "high", "type", "error"))
    .build();

channel.basicPublish("headers.exchange", "", props, "严重错误".getBytes());
// 这条消息会进入 queue.high.priority
性能提醒:Headers Exchange 的性能比 Direct 和 Topic 要差一些。因为每次匹配都要解析头部信息,如果消息量特别大(每秒上万条),建议优先考虑其他模式。

4.6 四种模式对比

模式 匹配依据 路由键 典型场景 性能
Direct 完全匹配 必须 点对点通知 高
Topic 通配符匹配 必须 日志分类、事件订阅 高
Fanout 忽略路由键 忽略 广播通知、配置更新 中
Headers 头部属性 忽略 复杂条件过滤 中低

4.7 核心逻辑流程图

下面这张图展示了四种交换机的工作流程。我建议你仔细看看,理解消息从生产者到消费者的完整路径。

RabbitMQ 交换机模式核心逻辑 生产者 消息 交换机 (Exchange) Direct Topic Fanout Headers 路由键/头部匹配 队列 (Queue) queue.order queue.payment queue.log 消费者A 消费者B 消费者C 图例说明: 生产者 交换机 队列 消费者 消息流向 绑定关系

从这张图可以看得很清楚:生产者把消息发给交换机,交换机根据规则匹配队列,队列再把消息推给消费者。每种交换机的匹配规则不同,但整体流程是一样的。

4.8 如何选择?我的建议

说了这么多,到底该用哪种?我个人的经验是:

  • 点对点通信:用 Direct。简单、高效、好理解。
  • 需要灵活订阅:用 Topic。通配符匹配能省不少事。
  • 全员广播:用 Fanout。别想太多,发就完了。
  • 条件特别复杂:用 Headers。但要做好性能下降的心理准备。

嗯,最后提醒一句:不管用哪种模式,记得给交换机和队列起个好名字。我见过有人用「aaa」「bbb」命名,三个月后自己都看不懂了。命名规范一点,后期维护能省不少心。

核心要点回顾:

  • Direct:路由键完全匹配绑定键
  • Topic:支持 * 和 # 通配符匹配
  • Fanout:忽略路由键,广播到所有队列
  • Headers:根据消息头部属性匹配
  • 交换机不存储消息,必须绑定队列

可可精品资料库