上一篇讲了 AMQP 的四要素,这一篇往细节走一步:消息从队列到消费者,到底是怎么交付的。这个问题表面简单,但它背后藏着消息队列最基础的一个分类——点对点(P2P)和发布订阅(Pub/Sub)。
很多人用 RabbitMQ 用了很久,分不清自己用的是哪种模型,更说不清「多个消费者监听同一个队列,消息会怎样」。这种模糊会在设计消费拓扑时埋坑:本该广播的消息变成了竞争消费,本该负载均衡的消息却被重复处理。这一篇把两种模型的边界讲清楚,并说明 RabbitMQ 如何用一套机制同时支持两者。
点对点:一条消息只被一个消费者处理
点对点模型的核心特征是:一条消息只能被一个消费者消费。
在 RabbitMQ 里,当一个队列有多个消费者时,默认就是点对点——队列会把消息轮询分发(round-robin)给各个消费者,每条消息只交给其中一个。假设队列里有 100 条消息、有 3 个消费者,那么每个消费者大约处理 33 条,不会重复。
这是典型的「负载均衡」语义:多个消费者分担一个队列的工作量。适用于任务分发场景——比如有 10 万张图片要转码,开 10 个转码 worker 共享一个队列,每个任务只被一个 worker 处理,大家分摊负载。
点对点模型的关键约束是消息不会重复处理。如果你发现「同一条消息被多个消费者都处理了一遍」,那不是 P2P 在工作,而是配置错了——要么误用了广播,要么消费者重复 ack 失败导致重投。
发布订阅:一条消息被所有订阅者处理
发布订阅模型的核心特征是:一条消息会被所有订阅者各自处理一次。
在 RabbitMQ 里,Pub/Sub 不是靠「一个队列多个消费者」实现的,而是靠多个队列实现。每个订阅者有自己独立的队列,这些队列都绑定到同一个 Exchange(通常是 fanout 或 topic)。Exchange 把消息复制到所有绑定的队列,每个队列各自被自己的消费者消费。
举例:订单创建后,积分服务、通知服务、统计服务都要处理。正确做法是给三个服务各建一个队列,都绑定到 order.exchange。一条订单消息进来,Exchange 把它复制三份,分别投到三个队列,三个服务各自消费自己队列里的那条消息。这就是发布订阅——一份消息,多方各处理一次。
这里的关键认知是:RabbitMQ 的 Pub/Sub 是「队列级别」的隔离,不是「消费者级别」的。同一个队列里的多个消费者是竞争(P2P),不同队列之间才是各自独立(Pub/Sub)。
那条缝:Queue 是共享的还是独占的
把两种模型放一起,就能看清那条缝——消息会不会重复处理,取决于消费者是共享一个队列,还是各有独立队列。
| 维度 | 点对点(共享队列) | 发布订阅(独立队列) |
|---|---|---|
| 队列数量 | 1 个 | 每个订阅方 1 个 |
| 消息分发 | 轮询给各消费者 | 复制到各队列 |
| 是否重复处理 | 否,每条只处理一次 | 是,每个订阅方各处理一次 |
| 典型场景 | 任务分发、负载均衡 | 事件通知、多方订阅 |
| Exchange 选择 | direct(或默认) | fanout / topic |
这条缝是设计消费拓扑时最重要的判断点:问自己「这条消息是希望被一个消费者处理,还是希望多个订阅方各处理一次」。答案不同,拓扑设计完全不同。
最常见的错误是:本意要 Pub/Sub,却让多个服务连到同一个队列上——结果是消息被轮询分给各服务,积分服务收一部分、通知服务收一部分,每个服务都只处理了「自己那份」,漏掉了本该全部处理的消息。这种 bug 很隐蔽,因为不报错,只是业务数据对不上。
一个队列多个消费者:还能精细控制
点对点模型里,「一个队列多个消费者」默认是轮询。但 RabbitMQ 还允许更精细的控制——prefetch(预取)。
如果不设 prefetch,RabbitMQ 会把消息尽量均匀推给所有消费者,但这可能导致负载不均:一个消费者处理慢、积了一堆消息,另一个消费者闲着却拿不到。原因是 RabbitMQ 默认是「推送」语义,会提前把消息发出去。
设了 prefetch 之后,RabbitMQ 对每个消费者「在途未确认」的消息数量做限制。比如 prefetch=1,表示每个消费者最多同时持有 1 条未确认的消息,处理完 ack 后才发下一条。这样处理快的消费者会自然多拿消息,处理慢的少拿,实现了能者多劳的负载均衡。
prefetch 是 RabbitMQ 消费侧调优的核心参数。设太大,消息会堆积在消费者内存里,处理慢的消费者拖累整体;设太小(比如 1),又会让吞吐下降(每条消息都要一次网络往返)。这个话题在阶段四会专门展开,这里先建立「prefetch 控制分发节奏」的认知。
消息确认:消费的可靠性基石
聊到消费者,绕不开消息确认(ack)。在点对点模型里,ack 是保证「消息不丢」的关键机制。
消费者从队列拿到消息后,消息并不会立刻从队列删除——它变成「未确认」(unacked)状态。只有消费者显式发送 ack 后,队列才会真正删除这条消息。如果消费者在 ack 之前崩溃或断开连接,队列会发现这条消息没有被确认,于是重新投递给另一个消费者。
这就是 RabbitMQ 消息可靠性的基础闭环:消息只有被成功处理并 ack,才算真正消费完;否则会重新投递。配合生产端的 confirm 和 broker 的持久化,构成了 RabbitMQ「消息不丢」的完整链条(下一篇详谈)。
这里有个关键选择:auto-ack vs manual-ack。
- auto-ack(自动确认):消费者拿到消息就视为确认,队列立刻删除。吞吐高,但消息可能没处理完就丢了——消费者拿了消息还没处理就崩溃,消息就没了。只适合「丢了也无所谓」的场景。
- manual-ack(手动确认):消费者处理完业务逻辑后,显式 ack。可靠,但要处理「忘了 ack 导致消息一直 unacked」的问题。
生产环境的核心链路几乎都用 manual-ack。宁可吞吐低一点,也不能丢消息。
死信:处理不了的消息去哪了
点对点消费里,还有一类情况:消息被消费了,但处理失败,重试几次还是失败。这些「处理不了」的消息怎么办?
如果一直 nack 重试,这条坏消息会卡住消费者,导致后面的正常消息被阻塞(毒丸消息,poison pill)。RabbitMQ 的解法是死信队列(DLX):给队列配置一个死信交换器,当消息满足「被 reject 且不重投」「TTL 过期」「队列满」这三个条件之一时,会被转发到死信交换器,再路由到死信队列。
死信队列让「处理失败的消息」有了去处,既不阻塞正常消费,又不丢失,方便后续人工介入或补偿重试。这是 RabbitMQ 消费可靠性的延伸闭环,阶段二会专门展开。
收束:模型决定拓扑
这一篇的核心是一个判断:点对点还是发布订阅,取决于消费者是共享队列还是各有队列。这个判断决定了整个消费拓扑的设计——是用一个队列负载均衡,还是用多个队列各自隔离。
RabbitMQ 的精巧之处在于,它用一套「Exchange + Queue + Binding」机制,同时承载了这两种模型,靠的是「队列是否共享」这个变量。下一篇会继续往可靠性走,讲消息队列最核心的工程问题之一:怎么保证消息不丢、不重、有序。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。我相信 AI 是程序员的最佳搭档。想跟完这套「图解 RabbitMQ」,欢迎关注公众号 「十三Tech」。

