RabbitMQ 里那七个经典模式——Simple、Work Queues、Publish/Subscribe、Routing、Topics、RPC、Publisher Confirms——是所有做后端的人迟早要啃一遍的“基本功套餐”。我见过太多人面试前背概念背得滚瓜烂熟,真到项目里要选型的时候,却连 Fanout 和 Direct 该用哪个都拿不准。这篇文章就是把我自己从入门到落地,把这七种模式逐个跑通、踩坑、复盘的过程完整记录下来,配合 Java 客户端代码,把每种模式解决什么问题、交换机怎么绑、消息怎么不丢,一次讲透。不管你是刚接触消息队列的新手,还是想系统梳理一遍的老手,这套内容都能直接用。
1. 项目背景与整体设计思路
1.1 这套模式体系到底在解决什么问题
RabbitMQ 这么多模式,本质上都是在回答一个问题:生产者产生的消息,应该以什么规则交给哪些消费者?
你可以把 RabbitMQ 想象成一个快递分拨中心。生产者是各个发货商家,消费者是末端配送员,而交换机(Exchange)就是分拨中心里那张分拣台。Simple 模式是“点对点专线”,一个商家指定一个配送员;Work Queues 是“多个配送员抢同一个仓库的件”;Fanout 是“广播站”,一个通知所有配送员都要知道;Direct 是“按标签分发”,只有标签匹配的配送员才接单;Topic 是“按规则模糊匹配”,比 Direct 更灵活;RPC 模式则是“配送员送完件还要把回执单带回来”;而发布确认是“商家发货后要确认快递公司真的收件了”。
把这套图景装进脑子,后面所有代码都只是翻译。RabbitMQ 的客户端 API 其实非常薄,核心就四件事:建连接、开信道、声明交换机/队列、发布或消费消息。所谓七种模式,玩来玩去就是交换机类型和绑定关系的排列组合。
我在实际项目里最常见的误区是:很多人一上来就直接用默认交换机,把消息发到队列名上,导致后面要加多个消费者、要按级别路由时,代码推倒重来。所以理解这套模式体系的真正价值,不是背七种 API 写法,而是建立一套“消息路由”的思维模型——知道每种交换机适合什么业务形态,才不会在架构设计时拍脑袋。
1.2 模式选型的核心逻辑:抓住交换机这张“分拣台”
RabbitMQ 的消息路由模型非常清晰,就三张表:
- 生产者把消息发给交换机,并带上一个路由键(Routing Key);
- 交换机根据自身类型和绑定的规则,决定把消息投递到哪些队列;
- 消费者只从队列里取消息,永远不直接和交换机打交道。
模式与交换机类型的对应关系是选型时的第一张对照表:
| 模式 | 交换机类型 | 路由规则 | 典型场景 |
|---|---|---|---|
| Simple | 默认交换机 | 路由键=队列名,精确匹配 | 入门示例、简单任务 |
| Work Queues | 默认交换机 | 同上 | 任务分发、削峰填谷 |
| 发布/订阅 | Fanout | 忽略路由键,广播到所有绑定队列 | 全局通知、日志广播 |
| 路由 | Direct | 路由键与绑定键完全相等 | 按级别过滤日志 |
| 通配符 | Topic | 路由键匹配 *、# 模式 |
按业务域拆分消息 |
| RPC | Direct(通常是) | 配合 replyTo 与 correlationId | 同步调用异步化 |
还有一个最容易被忽略的点:交换机和队列都是需要先声明的。声明操作是幂等的,同一个名字重复声明不会报错,但如果参数对不上会抛异常(比如先声明了持久化队列,后面又用非持久化参数声明同名队列,会直接 406)。我早期在这个坑里栽过好几次,所以后面所有代码示例都会保持声明参数一致。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:从安装到跑通第一个队列
2.1 两种部署方式:Docker 与 Windows 本机安装
RabbitMQ 基于 Erlang 开发,所以最麻烦的一步其实是环境依赖。我推荐两条路,根据自己的系统选。
Docker 方式(最省心)。一条命令就能起一个带管理界面的实例:
bash复制docker run -d --name rabbitmq \
-p 5672:5672 -p 15672:15672 \
rabbitmq:3.12-management
这里两个端口是必须暴露的:5672 是 AMQP 协议端口,客户端连接用;15672 是管理控制台端口,浏览器访问用。如果做集群,通常还要暴露 25672(节点间通信)和 4369(Erlang 节点发现),但单机学习阶段用不到。启动后访问 http://localhost:15672,默认账号密码是 guest/guest。
Windows 本机安装。需要先装 Erlang 再装 RabbitMQ,版本必须匹配,官网的版本对应关系表一定得看,我见过太多人因为 Erlang 版本过高导致 RabbitMQ 起不来的情况。装完后默认服务会自动启动,然后在命令行执行:
bash复制rabbitmq-plugins enable rabbitmq_management
这条命令开启管理插件,否则只有 5672 端口能用,看不到任何可视化界面。Windows 下执行命令要用管理员权限,否则插件写入会失败。
强烈建议装好后第一件事:打开管理控制台,把 guest 账号的权限范围搞清楚。guest 默认只能从 localhost 访问,如果你在 Docker 容器里跑、宿主机连,或者局域网内其他机器连,会一直报 ACCESS_REFUSED。解决办法是新建一个专用账号并授权:
bash复制rabbitmqctl add_user dev dev123
rabbitmqctl set_permissions -p / dev ".*" ".*" ".*"
这个坑在实战中出现的频率极高,先建好账号能省一堆排查时间。
2.2 核心概念:连接、信道、队列、交换机
RabbitMQ 客户端里有两个最容易混淆的对象:Connection 和 Channel。
Connection 是 TCP 长连接,一个应用通常只需要建一个,因为 TCP 握手和 TLS 协商的开销都不小。Channel 是建立在 Connection 之上的虚拟信道,可以理解成 TCP 连接里的“复用通道”。生产环境里每个线程都应该用独立的 Channel,而不是共享同一个。官方文档的原话是:Channel 不是线程安全的。这个细节在写多线程生产者时特别重要,很多人并发一高就报 channel is already open 之类的诡异错误,多半就是 Channel 被多线程共用了。
队列声明时最关键的三个参数:durable(持久化)、exclusive(独占)、autoDelete(自动删除)。
durable=true表示队列元数据持久化,RabbitMQ 重启后队列还在;exclusive=true表示只有当前连接能用,连接关闭队列即删除,通常用于临时队列;autoDelete=true表示最后一个消费者断开后自动删除队列。
这三个参数搭配起来,就是后面 RPC 模式里临时队列的底层原理。我建议你在部署第一个 Demo 之前,先在控制台里手动创建几个不同参数的队列,观察它们在状态页里的差异,比直接背文档形象得多。
3. 基础模式拆解:Simple 与 Work Queues
3.1 Simple 模式:最短路径理解消息流转
Simple 模式是所有模式的地基。它不声明任何交换机,直接用默认交换机(名字是空字符串 ""),路由键就是队列名。所以生产者把消息发到 "" 交换机、路由键写 hello,消息就会进入名为 hello 的队列。
生产者代码:
java复制ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare("hello", false, false, false, null);
channel.basicPublish("", "hello", null, "Hello RabbitMQ".getBytes());
System.out.println("消息已发送");
}
消费者代码:
java复制ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare("hello", false, false, false, null);
channel.basicConsume("hello", true, (consumerTag, delivery) -> {
System.out.println("收到消息: " + new String(delivery.getBody()));
}, consumerTag -> {});
}
这段代码能跑通,你就已经掌握了 RabbitMQ 的最小闭环。但我要提醒一个隐藏细节:这里的 basicConsume 第二个参数 true 表示自动确认(autoAck),意味着消费者一收到消息就告诉 Broker“我处理完了”,不管业务逻辑是否真正成功。这在 Demo 里无所谓,在生产环境里等于“消息随便丢”。后面 Work Queues 会专门讲怎么改掉这个坏习惯。
3.2 Work Queues:轮询分发与公平分发
Work Queues 解决的是“一条队列多个消费者”的任务分发问题。比如有一批图片处理任务,一台机器处理太慢,就启动多个消费者实例同时消费同一个队列。
默认情况下,RabbitMQ 采用轮询分发(Round-Robin):消息按顺序轮流发给每个消费者,不管你前一条处理得多慢。这种策略有个很现实的问题:如果消费者 A 处理一条消息要 10 秒,消费者 B 处理一条只要 1 秒,两个消费者还是会各拿一半消息,B 大部分时间在空转,A 则积压到天荒地老。
解决办法是公平分发(Fair Dispatch),核心就两行:
java复制channel.basicQos(1);
channel.basicConsume("task_queue", false, (consumerTag, delivery) -> {
// 模拟业务处理
Thread.sleep(3000);
// 处理完成后手动确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {});
basicQos(1) 告诉 RabbitMQ:在收到我对上一条消息的确认之前,不要再给我发新消息。这样一来,处理快的消费者会不断被填充任务,处理慢的消费者则不会被硬塞,整体吞吐反而更高。
basicConsume 的第二个参数改成 false,表示关闭自动确认,改用 basicAck 手动确认。这背后是一个非常重要的理念:消息队列只能保证“送达”,不能保证“处理成功”,这两者之间隔着一次手动确认。
这里还有个细节,basicAck 的第二个参数 multiple 如果设为 true,表示确认当前 deliveryTag 之前所有未确认的消息。批量确认能减少网络往返,但要注意别误确认掉还没处理完的消息。我一般在生产环境用 false,逐条确认,省心且不容易出 bug。
3.3 手动确认与消息持久化:真正不丢消息的底线
很多新手以为关闭 autoAck 就万事大吉了,其实还差一步。如果 RabbitMQ 服务重启,队列和消息都会丢失。要保证消息不丢,需要两道防线:
第一道:队列和消息都要持久化。队列声明时加 durable=true:
java复制channel.queueDeclare("task_queue", true, false, false, null);
发布消息时,消息属性也要标记为持久化:
java复制channel.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
注意,PERSISTENT_TEXT_PLAIN 只是把消息标记为持久化,但 RabbitMQ 并不会每条消息都立刻刷盘,它有自己的缓冲区。真要达到严格不丢,还得配合后面的发布确认机制。
第二道:消费者异常时拒绝消息而不是丢掉。如果业务处理失败,可以调用 basicNack 或 basicReject:
java复制try {
// 处理业务
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
} catch (Exception e) {
// requeue=true:消息重新回到队列,稍后再投递
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
}
这里有个非常经典的坑:如果业务逻辑本身有 bug,消息每次消费都失败,然后你又把 requeue 设为 true,消息会无限循环进入队列,把 CPU 和日志系统打爆。我见过线上事故就是这么发生的。更稳妥的做法是给队列配置死信交换机,把多次失败的消息转到死信队列里,人工介入处理。这也是“死信队列”概念的由来——你可以设置消息 TTL,比如 30 分钟没被成功消费就自动进入死信队列,再写个定时任务去捞死信做补偿。
4. 三种交换机模式:发布订阅、路由、通配符
4.1 Fanout 发布订阅:一条消息广播给所有人
发布订阅模式用的交换机类型是 Fanout。它的逻辑最简单:忽略路由键,把消息广播给所有绑定的队列。就像公司群发通知,所有人都会收到。
生产者:
java复制channel.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
channel.basicPublish("logs", "", null, "系统公告:今晚停机维护".getBytes());
消费者这边有个常见用法:不自己命名队列,而是让 RabbitMQ 生成一个随机临时队列,然后绑定到交换机:
java复制String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, "logs", "");
queueDeclare() 不传参数时,RabbitMQ 会生成一个类似 amq.gen-xxxx 的随机队列名,并且是 exclusive 的,连接断开就删除。这种队列天然适合“临时订阅”的场景。
Fanout 最常见的坑是:先启动了消费者,再启动生产者,结果一条消息都没收到。这不是 bug,而是 Fanout 的语义如此——消息发出去就立刻广播到当时存在的队列,如果队列还没绑定或者已经删除了,消息就丢了。开机广播、动态订阅这类场景,Fanout 非常合适;要做可靠异步任务,还是得回到 Work Queues。
4.2 Direct 路由:按路由键精确匹配
Direct 交换机比 Fanout 精细一步:路由键和绑定键完全相等,消息才会进入对应的队列。这就好比快递分拣,只有地址标签写“技术部”的件才会进技术部的筐。
生产者按级别发布日志:
java复制channel.exchangeDeclare("direct_logs", BuiltinExchangeType.DIRECT);
channel.basicPublish("direct_logs", "error", null, "数据库连接失败".getBytes());
channel.basicPublish("direct_logs", "warning", null, "磁盘使用率 85%".getBytes());
消费者可以只绑定自己关心的级别:
java复制String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, "direct_logs", "error");
channel.queueBind(queueName, "direct_logs", "warning");
这个例子里,绑定 error 和 warning 的消费者只会收到两种消息,info 级别的消息不会被投递进来。
Direct 模式在实践里要特别注意:一个队列可以绑定多个键,多个队列也可以绑定同一个键。前者是“一个消费者订阅多种消息”,后者是“多个消费者分担一种消息”。这两个场景的业务含义完全不同,一个是过滤,一个是负载均衡,设计绑定关系前先想清楚自己要的是哪个。
4.3 Topic 通配符:* 与 # 的灵活路由
Topic 交换机是实际业务里最常用的类型,它用两个通配符做模式匹配:
*:匹配一个单词(点号分隔的一段);#:匹配零个或多个单词。
比如一条消息的路由键是 user.login.success,那么绑定键 user.*.success 能匹配,user.# 能匹配,user.login.* 也能匹配,但 user.* 不能匹配(因为 * 只能代表一个单词,而 user.login.success 是三个单词)。
典型的日志场景:
java复制channel.exchangeDeclare("topic_logs", BuiltinExchangeType.TOPIC);
channel.basicPublish("topic_logs", "order.created", null, "订单创建成功".getBytes());
channel.basicPublish("topic_logs", "payment.refund.failed", null, "退款失败".getBytes());
消费者按业务域订阅:
java复制// 只关心订单域的所有消息
channel.queueBind(queueName, "topic_logs", "order.*");
// 关心所有失败消息
channel.queueBind(queueName, "topic_logs", "*.failed");
// 关心全部消息
channel.queueBind(queueName, "topic_logs", "#");
Topic 模式的最大价值是把路由键设计变成一种协议。你可以在消息的 routing key 里编码多级业务信息,比如 环境.业务域.事件类型.结果,消费者就能用绑定键灵活地订阅任意组合。我建议团队内部把 routing key 的命名规范写进开发文档,否则半年后没人能看懂绑定的模式是什么意思。
这里我踩过一个具体的坑:* 只能匹配一个单词,但很多人把它理解成“任意字符”,结果 order.* 匹配不上 order.created.success,排查半天才发现是通配符语义理解错了。调试时可以到管理控制台的 Exchanges 页面,点进交换机看 Binding 列表,再用 Publish message 功能手动发一条测试消息,能直观看到消息进到了哪些队列。
4.4 三种交换机模式对比速查
| 对比项 | Fanout | Direct | Topic |
|---|---|---|---|
| 匹配规则 | 全部广播 | 完全相等 | 通配符模糊匹配 |
| 路由键作用 | 忽略 | 精确匹配 | 模式匹配 |
| 灵活度 | 最低 | 中 | 最高 |
| 复杂度 | 最低 | 中 | 最高 |
| 适用场景 | 全局广播 | 按类型过滤 | 按多级规则订阅 |
我的经验是:能用 Direct 说清楚的需求就别用 Topic,能用一个交换机解决的就别拆多个交换机。消息路由的复杂度是架构负债,每多一条绑定规则,将来排查问题就多一分成本。
5. RPC 模式与发布确认机制
5.1 RPC 模式:用消息队列做同步调用
RabbitMQ 的 RPC 模式有点反直觉:消息队列明明是异步的,却可以用来实现同步调用。原理是让客户端在发消息时带上两个特殊属性,然后阻塞等待结果返回:
replyTo:指定一个临时队列,服务端处理完后把结果发到这里;correlationId:关联 ID,用来匹配请求和响应,因为同一个临时队列里可能同时等多个响应。
客户端实现:
java复制String corrId = UUID.randomUUID().toString();
String replyQueueName = channel.queueDeclare().getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
channel.basicPublish("", "rpc_queue", props, "参数数据".getBytes());
// 阻塞等待响应
BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response.offer(new String(delivery.getBody()));
}
}, consumerTag -> {});
String result = response.take();
服务端实现:
java复制channel.queueDeclare("rpc_queue", true, false, false, null);
channel.basicQos(1);
channel.basicConsume("rpc_queue", false, (consumerTag, delivery) -> {
String response = handleRequest(new String(delivery.getBody()));
AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder()
.correlationId(delivery.getProperties().getCorrelationId())
.build();
channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes());
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {});
这段代码看起来不多,但每一步都有讲究。客户端必须校验 correlationId,否则服务端响应乱序时会串消息;服务端拿到请求后要把 replyTo 里的队列名原样用于响应,而且一定记得手动 basicAck,否则消息会重复派发。
我的建议是:这种 RPC 模式能不用就不用。它本质上是用消息队列模拟 HTTP 同步调用,徒增复杂度,还丧失了消息队列异步削峰的优势。真实业务里如果需要同步返回值,直接用 HTTP 或 gRPC 更合适。了解 RPC 模式是为了面试和读源码,不是为了上线。
5.2 发布确认机制:生产者怎么知道消息真的发出去了
聊完消费端确认,再看生产端确认。RabbitMQ 的发布确认机制(Publisher Confirms)就是为了解决“消息发出去了,但 Broker 到底收到没有”的问题。
开启方式一条命令:
java复制channel.confirmSelect();
之后每发一条消息,Broker 都会回一个确认(ack)或未确认(nack)。最简单的用法是同步确认:
java复制channel.confirmSelect();
channel.basicPublish("", "task_queue", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
if (channel.waitForConfirms()) {
System.out.println("消息确认成功");
}
waitForConfirms() 会阻塞等待 Broker 的确认。如果消息在持久化到磁盘之前 Broker 就宕机了,或者消息被路由到不存在的队列且无法投递,会返回 false 或抛出异常。更精细的用法是加监听器:
java复制channel.confirmSelect();
channel.addConfirmListener((deliveryTag, multiple) -> {
// ack:消息已确认
System.out.println("确认, tag=" + deliveryTag);
}, (deliveryTag, multiple) -> {
// nack:消息确认失败
System.out.println("失败, tag=" + deliveryTag);
});
监听器方式不阻塞发消息的线程,适合高吞吐场景,但需要自己维护一个未确认集合,配合 deliveryTag 判断哪些该重发。这算是 RabbitMQ 进阶里最实用的能力之一,也是“事务消息”之外的另一个选择——RabbitMQ 的事务机制 txSelect() 性能比发布确认差太多,生产上没人用,确认机制才是正解。
有个细节我要专门提醒:waitForConfirms 支持批量模式,waitForConfirms(5000) 传超时时间,可以避免无限阻塞。但确认返回的只是“Broker 收到了”,并不代表消费者处理成功,消费端的 basicAck 仍然不能省。生产者和消费者两端的确认是独立的两条链路,各管各的。
6. 常见问题与排查技巧实录
6.1 高频问题速查表
我把这几年在 RabbitMQ 上遇到的高频问题整理成一张表,基本都是搜索热词里反复出现的:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
连接报 ACCESS_REFUSED |
guest 用户只允许 localhost 访问 | 新建用户并授权,或用 rabbitmqctl 设置 guest 的 loopback 权限 |
| Docker 容器内连接失败 | 只映射了 15672 没映射 5672 | 确认 -p 5672:5672 已暴露 |
队列声明报 406 PRECONDITION_FAILED |
同名队列重复声明但参数不一致 | 删掉旧队列,或统一声明参数 |
| 消息发出去但消费者收不到 | 交换机/队列绑定关系不对,或 Fanout 下队列还没绑定 | 到管理控制台确认 Exchange → Binding 关系 |
| 消费者处理慢且消息大量堆积 | 没有设置 basicQos,或消费者数量不够 |
设置 basicQos(1),结合压测扩容消费者 |
| 服务重启后消息丢失 | 队列和消息都没有持久化 | 队列 durable=true,消息用持久化属性 |
| 消费者重复收到同一条消息 | 消费后没手动 ack,或 ack 前连接断开 | 确认业务完成后调用 basicAck |
| 生产者以为发出去了但队列里没有 | 没有开启发布确认,消息路由到交换机失败 | confirmSelect 并检查 basicPublish 返回值 |
| 集群节点间无法通信 | 只暴露了 5672 和 15672 | 集群需要额外暴露 25672、4369 端口 |
这里面要特别展开两个。第一个是 消息倾斜问题:没设 basicQos 时,RabbitMQ 默认轮询分发,可能造成慢消费者堆积。我实测过一个场景,同样 10 万条消息,两个消费者处理速度差 5 倍,开了 basicQos(1) 之后整体耗时从 40 分钟降到 18 分钟,效果立竿见影。
第二个是 连接和信道的生命周期。新手最常见的写法是每条消息都新建一个 Connection,性能极差。正确做法是 Connection 复用、Channel 按线程创建。我曾经写过一个简单的压测脚本,复用连接比每次新建连接吞吐高了一个数量级。
6.2 实操心得与避坑经验
最后分享几个我长期坚持的实操习惯。
先把管理控制台玩熟。任何模式下,我都建议你先到控制台的 Exchanges 和 Queues 页面,手动发几条消息、观察消息怎么流转。控制台里能看到每个队列的 Ready、Unacked、Total 三个数字——Ready 是等待消费的消息数,Unacked 是已投递但未确认的消息数。如果 Unacked 一直很高,说明消费者处理能力不足;如果 Ready 涨得飞快,说明生产者速率远大于消费速率。这两个数字就是消息队列的“体温计”。
日志里永远打印投递标签。消费端的 deliveryTag 是排查重复消费问题的关键线索。如果一条消息被打了两遍,日志里会看到相同的 deliveryTag 出现在不同时间点,这时候基本可以确定是 ack 超时或者连接抖动导致消息重新投递。
给队列和交换机起名要带业务语义。我见过太多 queue1、queue2 这种名字,三个月后没人知道是干嘛的。建议格式:业务域.事件名.队列用途,比如 order.created.notify_queue,交换机用 amq.topic 风格加前缀,比如 ex.order。命名规范虽然简单,但在排查问题时能省一半脑力。
生产环境一定要有监控告警。至少盯三个指标:队列积压数(Ready 数)、Unacked 数、连接数。积压数持续上涨一般就是消费者出问题了;连接数异常波动通常说明客户端代码有连接泄漏。RabbitMQ 管理 API 提供了 /api/queues 接口,可以写个脚本定时拉取,配合任何监控系统都能做告警。
我个人在实际操作中的体会是:消息队列这玩意儿,90% 的问题都出在“确认机制”上——要么没开确认,要么确认的时机不对。把消费端手动 ack、生产端 confirm、队列和消息持久化这三件事做扎实,RabbitMQ 的可靠性已经超过大部分业务系统的需求了。剩下那 10% 的坑,基本都是绑定关系没理清,回到交换机那张“分拣台”的图上重新捋一遍,通常都能找到答案。这套模式体系我建议你按顺序动手敲一遍,尤其是把 Fanout、Direct、Topic 三种交换机拿同一批消息分别跑一次,亲眼看到消息的分发差异,比看十遍文档都管用。
