4、RabbitMQ 交换机模式:原理与代码实现
消息队列的核心,说白了就是「生产者发消息,消费者收消息」。但消息怎么发、发给谁,这里面的门道可不少。RabbitMQ 用交换机(Exchange)来解决这个问题。我个人习惯把交换机想象成一个「智能分拣员」——它根据你设定的规则,把消息投递到对应的队列里。
今天咱们就聊聊四种交换机模式:Direct、Topic、Fanout、Headers。每种模式我都踩过坑,咱们边聊边看代码。
4.1 交换机的基础概念
在动手写代码之前,先搞清楚几个关键角色:
- 生产者(Producer):发消息的人
- 交换机(Exchange):消息的
- 队列(Queue):消息的临时仓库
- 消费者(Consumer):取消息的人
- 路由键(Routing Key):消息的「地址标签」
- 绑定键(Binding Key):队列的「收件规则」
嗯,这里要注意:交换机和队列之间通过绑定键建立关系。消息到了交换机,交换机会拿消息的路由键去匹配队列的绑定键。匹配上了,消息就进队列;匹配不上,消息就丢了。
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 -> {});
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 只会收到第三条
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());
// 三个队列都会收到这条消息
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
4.6 四种模式对比
| 模式 | 匹配依据 | 路由键 | 典型场景 | 性能 |
|---|---|---|---|---|
| Direct | 完全匹配 | 必须 | 点对点通知 | 高 |
| Topic | 通配符匹配 | 必须 | 日志分类、事件订阅 | 高 |
| Fanout | 忽略路由键 | 忽略 | 广播通知、配置更新 | 中 |
| Headers | 头部属性 | 忽略 | 复杂条件过滤 | 中低 |
4.7 核心逻辑流程图
下面这张图展示了四种交换机的工作流程。我建议你仔细看看,理解消息从生产者到消费者的完整路径。
从这张图可以看得很清楚:生产者把消息发给交换机,交换机根据规则匹配队列,队列再把消息推给消费者。每种交换机的匹配规则不同,但整体流程是一样的。
4.8 如何选择?我的建议
说了这么多,到底该用哪种?我个人的经验是:
- 点对点通信:用 Direct。简单、高效、好理解。
- 需要灵活订阅:用 Topic。通配符匹配能省不少事。
- 全员广播:用 Fanout。别想太多,发就完了。
- 条件特别复杂:用 Headers。但要做好性能下降的心理准备。
嗯,最后提醒一句:不管用哪种模式,记得给交换机和队列起个好名字。我见过有人用「aaa」「bbb」命名,三个月后自己都看不懂了。命名规范一点,后期维护能省不少心。
核心要点回顾:
- Direct:路由键完全匹配绑定键
- Topic:支持 * 和 # 通配符匹配
- Fanout:忽略路由键,广播到所有队列
- Headers:根据消息头部属性匹配
- 交换机不存储消息,必须绑定队列