系列导航:

这一组文章统一围绕同一个业务事件展开:订单服务完成写库后,向 RabbitMQ 发布一条 order.created。本篇不讨论交换机类型,也不展开监控与治理,只回答一个更基础的问题:Broker 到底在什么时刻把消息算作“还在队列里”、“已经交给消费者”、“可以删除了”。

从同一个 order.created 开始

{
  "eventId": "01JABCXYZ9Q4T2R8M7N6P5K4H3",
  "eventType": "order.created",
  "occurredAt": "2026-08-05T10:00:00Z",
  "orderId": "ORD-20260805-001",
  "customerId": "C10086",
  "amount": 299.00
}

假设库存服务从 orders.inventory 队列消费这条消息。它拿到消息后会做两件事:预留库存,以及把处理结果写入自己的数据库。只有这两步都成功,我们才希望 RabbitMQ 真正删掉这条消息。

readyunacked 和删除到底是什么

可以把一条消息在消费侧看到的生命周期压缩成三步:

  1. ready:消息已经进入 Queue,但还没有被投递给某个 Consumer。
  2. unacked:Broker 已经把消息投递出去了,但还没收到该次投递对应的确认。
  3. 删除:Consumer 发回 ack,Broker 才把这条消息从队列语义上移除。

沿着 order.created 这条消息看,过程通常是这样的:

确认状态机与 prefetch 在途窗口上半部分展示 ready 到 unacked 后的 ack、重新入队和死信分支,下半部分展示 prefetch 等于三时三个在途槽位占满后暂停投递。DELIVERY STATE · 确认决定消息如何收尾ready仍在 Queue 等待投递unacked已投递,尚未确认Broker 投递ack → 删除业务成功,Broker 收尾reject / nack(false)丢弃或进入 DLXnack(requeue: true) → 回到 readyPREFETCH = 3 · 限制同时在途的未确认数orders.inventoryQueue · ready下一条等待窗口释放CONSUMER IN-FLIGHT WINDOWslot 1unackedslot 2unackedslot 3unacked3 / 3 已占满 → Broker 暂停继续投递Consumer处理并确认任一 slot ack / nack 后释放,Queue 中下一条消息才可进入窗口。确认状态机与 prefetch 在途窗口移动布局中的 ready、unacked、确认结果与 prefetch 三槽窗口。DELIVERY STATEreadyQueue 中等待unacked已投递未确认nack(true) · 回队ack → Broker 删除消息reject / nack(false)丢弃或进入 DLXPREFETCH = 3Queue · ready下一条消息等待窗口释放CONSUMER IN-FLIGHT WINDOWslot 1 · unackedslot 2 · unackedslot 3 · unacked3 / 3 已占满 · 暂停投递任一确认后释放 slotprefetch 限制未确认数,不是每秒处理速度
确认状态机与 prefetch 在途窗口确认决定消息何时离开 Broker,prefetch 决定消费者一次最多借走多少条尚未确认的消息。
消息从 ready 进入 unacked;ack 后删除,要求重回队列的 nack 返回 ready,拒绝重排的消息进入丢弃或死信路径。prefetch 限制单个消费者同时持有的未确认消息数。

这里最容易混淆的点有两个:

  • unacked 不是“快处理完了”,而是“已经交付,但结果未确认”。
  • 消息被 Consumer 收到,不等于消息已经安全删除;在手动确认模式下,删除动作发生在 ack 到达 Broker 之后。

因此,当你在管理界面里看到 ready 很少、unacked 很多时,往往不是“RabbitMQ 卡住了”,而是 Consumer 已经接了不少活,但还没处理完或还没确认。

为什么 autoAck 经常把风险提前兑现

autoAck: true 的含义不是“业务成功后自动确认”,而是 Broker 一投递就认为这次消费完成。也就是说,order.created 一旦被发到 Consumer 连接上,Broker 就可以立即删除它。

这会带来一个很常见的风险窗口:

  1. Broker 把消息发给 Consumer。
  2. 因为开启了 autoAck,Broker 立刻删除消息。
  3. Consumer 还没来得及写库存表,进程就崩了,或者下游数据库超时。
  4. 结果变成“消息已经没了,但业务并没有完成”。

所以,autoAck 只适合真正可以接受丢处理机会的场景,例如低价值日志、临时缓存刷新通知,或者你已经在更上层做了其他容错。对于订单、库存、支付这类业务消息,通常应使用手动确认,把删除时点推迟到业务成功之后。

acknackreject 分别在表达什么

手动确认模式下,消费者其实是在对 Broker 说“这次投递该怎么收尾”。

操作表达的意思常见用途
ack这次投递已经成功处理,可以删除业务完成、状态已落库
nack这次投递没有成功,请按参数决定是否重回队列临时失败、批量否认、需要重试或停车
reject这次投递没有成功,但只针对单条消息语义上更老、更窄的单条拒绝

理解它们时,关键不是 API 名字,而是两件事:

  1. ack 只对本次投递负责。 如果业务已经成功,但 ack 在网络抖动时丢了,这条消息仍可能再次投递,所以消费者仍要为重复消费做准备。
  2. nack / reject 不是“回滚业务”。 它们只能告诉 Broker 这次投递没完成,不能撤销你已经写进数据库或调用过外部接口的副作用。

实践里可以这样区分:

  • 业务成功:ack
  • 临时性失败,且稍后重试仍有意义:nack(requeue: true)
  • 明确的不可恢复错误,不应继续原地重试:nack(requeue: false)reject(requeue: false)

如果团队代码里同时出现 nackreject,通常优先统一为 nack 更容易维护,因为它的表达能力更完整。

prefetch 控制的不是速度上限,而是同时在途的未确认数

prefetch 最值得记住的一句话是:它限制的是一个 Consumer 在没有确认前,最多能同时握住多少条消息。

例如把 prefetch 设为 10,含义不是“每秒只能处理 10 条”,而是:

  • 当这个 Consumer 手里已经有 10 条 unacked 消息时,Broker 会先暂停继续投递。
  • 只有等其中部分消息被 ack 或被否认后,投递窗口才会重新打开。

这会直接影响吞吐、内存占用和公平性:

  • prefetch 太大,单个 Consumer 可能提前拿走太多 order.created,造成其他实例分不到活,也会把更多消息压在应用进程内存里。
  • prefetch 太小,Broker 和 Consumer 会更频繁地往返确认,吞吐可能上不去。
  • 对单条处理时间较长、还会访问数据库或第三方接口的业务,先从较小值开始通常更稳妥。

prefetch 解决的是“在途工作量要不要设上限”,不是“自动帮你调优到最优吞吐”。

为什么消费者看起来很慢,但其实并不空闲

很多人第一次排查消费变慢时,会看到这样一种现象:

  • Queue 里还有不少 ready
  • 某个 Consumer 几乎收不到新消息
  • 业务侧却在持续跑 SQL、调接口、写日志

这往往不是 Consumer 空闲,而是 它的 prefetch 窗口已经被占满了。例如:

  1. Consumer 的 prefetch = 20
  2. Broker 已投递 20 条 order.created
  3. 这 20 条都在等库存库或下游接口返回,暂时还没 ack
  4. 在 Broker 看来,这个 Consumer 已经“手里拿满 20 条”,所以不会再继续推送

从应用线程看,它一直在忙;从 Broker 投递看,它又像“停住了”。这就是“看似慢但不空闲”的常见来源。

遇到这种情况,先别急着怀疑 RabbitMQ 本身,而要先判断:

  • 是不是 unacked 堆高了,说明消息已经到消费者手里?
  • 是不是单条业务耗时太长,导致确认迟迟发不回去?
  • 是不是 prefetch 设得过大或过小,让在途工作量与下游承载不匹配?

只要这三个问题没看清,单纯增加 Consumer 数量,往往只是把慢请求放大成更多并发慢请求。

本篇解决了什么:

  • 说明了消息在消费侧从 readyunacked 再到删除的关键状态,以及 acknackreject 分别在表达什么。
  • 解释了 autoAckprefetch 如何一起决定消息何时删除、消费者手里能同时在途多少工作。

下一篇:RabbitMQ 顺序与背压:消息积压时到底发生了什么(五)