RabbitMQ 消费控制:ack、nack、prefetch 与消息确认(四)
系列导航:
- 上一篇:RabbitMQ 路由设计:Exchange 与 Queue 怎么选(三)
- 当前篇:RabbitMQ 消费控制:ack、nack、prefetch 与消息确认(四)(本文)
- 下一篇:RabbitMQ 顺序与背压:消息积压时到底发生了什么(五)
这一组文章统一围绕同一个业务事件展开:订单服务完成写库后,向 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 真正删掉这条消息。
ready、unacked 和删除到底是什么
可以把一条消息在消费侧看到的生命周期压缩成三步:
ready:消息已经进入 Queue,但还没有被投递给某个 Consumer。unacked:Broker 已经把消息投递出去了,但还没收到该次投递对应的确认。- 删除:Consumer 发回
ack,Broker 才把这条消息从队列语义上移除。
沿着 order.created 这条消息看,过程通常是这样的:
这里最容易混淆的点有两个:
unacked不是“快处理完了”,而是“已经交付,但结果未确认”。- 消息被 Consumer 收到,不等于消息已经安全删除;在手动确认模式下,删除动作发生在
ack到达 Broker 之后。
因此,当你在管理界面里看到 ready 很少、unacked 很多时,往往不是“RabbitMQ 卡住了”,而是 Consumer 已经接了不少活,但还没处理完或还没确认。
为什么 autoAck 经常把风险提前兑现
autoAck: true 的含义不是“业务成功后自动确认”,而是 Broker 一投递就认为这次消费完成。也就是说,order.created 一旦被发到 Consumer 连接上,Broker 就可以立即删除它。
这会带来一个很常见的风险窗口:
- Broker 把消息发给 Consumer。
- 因为开启了
autoAck,Broker 立刻删除消息。 - Consumer 还没来得及写库存表,进程就崩了,或者下游数据库超时。
- 结果变成“消息已经没了,但业务并没有完成”。
所以,autoAck 只适合真正可以接受丢处理机会的场景,例如低价值日志、临时缓存刷新通知,或者你已经在更上层做了其他容错。对于订单、库存、支付这类业务消息,通常应使用手动确认,把删除时点推迟到业务成功之后。
ack、nack、reject 分别在表达什么
手动确认模式下,消费者其实是在对 Broker 说“这次投递该怎么收尾”。
| 操作 | 表达的意思 | 常见用途 |
|---|---|---|
ack | 这次投递已经成功处理,可以删除 | 业务完成、状态已落库 |
nack | 这次投递没有成功,请按参数决定是否重回队列 | 临时失败、批量否认、需要重试或停车 |
reject | 这次投递没有成功,但只针对单条消息 | 语义上更老、更窄的单条拒绝 |
理解它们时,关键不是 API 名字,而是两件事:
ack只对本次投递负责。 如果业务已经成功,但ack在网络抖动时丢了,这条消息仍可能再次投递,所以消费者仍要为重复消费做准备。nack/reject不是“回滚业务”。 它们只能告诉 Broker 这次投递没完成,不能撤销你已经写进数据库或调用过外部接口的副作用。
实践里可以这样区分:
- 业务成功:
ack - 临时性失败,且稍后重试仍有意义:
nack(requeue: true) - 明确的不可恢复错误,不应继续原地重试:
nack(requeue: false)或reject(requeue: false)
如果团队代码里同时出现 nack 和 reject,通常优先统一为 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 窗口已经被占满了。例如:
- Consumer 的
prefetch = 20 - Broker 已投递 20 条
order.created - 这 20 条都在等库存库或下游接口返回,暂时还没
ack - 在 Broker 看来,这个 Consumer 已经“手里拿满 20 条”,所以不会再继续推送
从应用线程看,它一直在忙;从 Broker 投递看,它又像“停住了”。这就是“看似慢但不空闲”的常见来源。
遇到这种情况,先别急着怀疑 RabbitMQ 本身,而要先判断:
- 是不是
unacked堆高了,说明消息已经到消费者手里? - 是不是单条业务耗时太长,导致确认迟迟发不回去?
- 是不是
prefetch设得过大或过小,让在途工作量与下游承载不匹配?
只要这三个问题没看清,单纯增加 Consumer 数量,往往只是把慢请求放大成更多并发慢请求。
本篇解决了什么:
- 说明了消息在消费侧从
ready到unacked再到删除的关键状态,以及ack、nack、reject分别在表达什么。 - 解释了
autoAck与prefetch如何一起决定消息何时删除、消费者手里能同时在途多少工作。