上一篇把可靠性三问(不丢、不重、有序)的边界划清了。这一篇落到实现层,拆开 RabbitMQ 保证「消息不丢」最核心的三个机制:confirm、return、ack

这三个机制经常被混在一起讲,让人搞不清谁负责什么。其实它们的分工很清晰,对应消息生命周期的三个不同节点。理解它们各自的角色和配合,才能在生产环境正确配置可靠投递,既不漏掉异常,又不至于把每条消息都当危险品处理。

三个机制各自的职责

先一句话定义清楚三个机制:

  • publisher confirm(生产者确认):回答「broker 到底有没有收到并持久化这条消息」。生产者发消息后等 broker 回 ack/nack,确保消息安全抵达 broker。
  • publisher return(退回):回答「这条消息被 broker 接收了,但路由不到任何队列怎么办」。消息到了 Exchange,但没有匹配的 Binding,消息就无处可去,broker 会退回给生产者。
  • consumer ack(消费者确认):回答「消费者有没有真正处理完这条消息」。消费者处理完业务后 ack,broker 才删除消息;没 ack 就崩溃,broker 重投。

注意 confirm 和 return 都在生产端,但职责不同:confirm 管「消息到没到 broker」,return 管「消息到了 broker 但路由失败」。这两者覆盖了生产侧的两个失败点。ack 在消费端,管消息有没有被成功处理。三者合起来,覆盖了消息从发出到消费完成的整条链路。

confirm:生产端的第一道确认

不开 confirm 时,生产者发消息是「fire and forget」——发出去就不管了,不知道 broker 收没收到。这在不可靠的网络里很危险:网络抖动、broker 崩溃,消息可能悄无声息地丢失。

开启 confirm 模式后,broker 对每条消息(或每批消息)回一个确认:

  • ack:broker 已收到消息,并且如果消息声明了持久化,已经把它写到磁盘(含副本)。生产者收到 ack 就可以放心,消息安全了。
  • nack:broker 在处理消息时出了问题(比如内部错误、磁盘满),消息没能持久化。生产者收到 nack 就知道要重发。

confirm 有两种使用方式:

  • 同步 confirm:发一条消息,阻塞等 ack。简单可靠,但每条消息一次往返,吞吐很低。适合对吞吐没要求、但每条都必须确认的场景。
  • 异步 confirm:生产者维护一个「已发未确认」的消息缓冲,broker 异步批量回 ack/nack。吞吐高,生产者要处理「哪些被确认了、哪些要重发」的状态管理。

生产环境一般用异步 confirm,配合一个基于消息 ID 的「待确认表」:消息发出时记入表,收到 ack 就删除,超时未收到 ack 就重发。这就是「本地消息表 + confirm」的经典可靠投递模式。

return:路由失败的兜底

confirm 解决了「消息到 broker」,但有一个 confirm 管不到的情况:消息到了 broker,但找不到任何匹配的队列

比如生产者往一个 topic Exchange 发消息,RoutingKey 是 order.refund,但当前没有任何 Binding 匹配这个 key(可能消费者都下线了,对应的队列被删了)。这条消息进了 broker,却无处可去。

默认情况下,broker 会把这种「路由不到」的消息静默丢弃。生产者完全无感知——它收到了 confirm 的 ack(因为消息确实到了 broker),却不知道消息其实被扔了。

return 机制就是为这种情况准备的。开启 mandatory 标志后,如果消息无法路由到任何队列,broker 不会丢弃,而是发一个 return 给生产者,带着原始消息和无法路由的原因。生产者收到 return 后可以决定:重试、改路由、告警、或者存起来人工处理。

return 和 confirm 配合的完整逻辑是:

  • 收到 ack:消息成功进入某个队列,任务完成。
  • 收到 nack:消息没被 broker 持久化,重发。
  • 收到 return:消息到了 broker 但路由失败,需要处理路由异常。

这三个回执覆盖了生产端所有可能的失败情况,没有盲区。

ack:消费端的确认

ack 这一端上一篇已经提过,这里补充它的几个关键细节。

消费者收到消息后,有三种回应方式:

  • ack:处理成功,broker 删除消息。
  • nack:处理失败,broker 根据 requeue 标志决定是重新入队(重试)还是直接丢弃(或转死信)。
  • reject:和 nack 类似,但 reject 一次只拒绝一条消息,nack 可以批量。

ack 的一个关键细节是必须是同一个 Channel 发的。消息从 Channel A 投递,就必须从 Channel A ack,不能跨 Channel ack,否则会触发协议错误导致连接断开。这是新手常踩的坑——在多线程环境里,不同线程拿了消息,却在错误的 Channel 上 ack。

另一个细节是 ack 的批量模式。消费者可以发一个带 multiple=true 的 ack,一次确认这条消息及之前所有未确认的消息。这能减少网络开销,但风险是一旦这条 ack 丢失,多条消息都要重投。生产环境通常关闭 multiple,逐条 ack 更安全。

三个机制怎么配合:一个完整链路

把三个机制串起来,一条消息的可靠投递链路是这样的:

confirm、return、ack 三角配合

  1. 生产者发消息,开启 confirm + mandatory。
  2. broker 收到消息,持久化(如果声明了),路由到匹配队列。
  3. 如果路由失败(mandatory 模式),broker 发 return,生产者处理路由异常。
  4. broker 处理完,发 confirm ack 给生产者。生产者收到后从「待确认表」删除。
  5. 消费者从队列拿到消息,处理业务逻辑。
  6. 处理成功,消费者 ack,broker 删除消息。
  7. 处理失败,消费者 nack/reject,消息重新入队或转死信。

这个链路里,每一个「消息可能消失」的点都有对应的机制兜底。任何一个环节出问题,消息都不会悄无声息地丢失,而是通过回执让相关方感知到并处理。

生产级配置模板

落到具体配置,一个生产级的可靠投递设置大致是:

生产端

  • 开启 confirm 模式(异步,配合本地消息表)。
  • 开启 mandatory 标志,注册 return callback 处理路由失败。
  • 消息声明持久化(deliveryMode=2)。
  • 发送到 durable 的 Exchange。

broker 端

  • Queue 声明为 durable。
  • 用 Quorum Queue 替代经典队列,消息多副本(阶段三详谈)。

消费端

  • manual-ack,关掉 auto-ack。
  • prefetch 设一个合理值(不要无限,避免消息堆积在消费者内存)。
  • 注册 nack/reject 处理,失败的消息转死信队列重试或人工介入。

这套配置不是「开得越多越好」,而是覆盖了所有失败点、又不过度牺牲性能的平衡配置。它的吞吐会比「全关」低不少,但在订单、支付这类不能丢消息的核心场景,这是必须的代价。

收束:闭环没有盲区

confirm、return、ack 这三个机制合起来,覆盖了消息从发出到消费完成的完整链路,没有盲区。confirm 管「到 broker」,return 管「broker 内路由」,ack 管「被消费」。理解了它们的分工,配置可靠投递就不是背参数,而是按失败点逐一兜底。

到这里,阶段一「重新认识 RabbitMQ」的六篇就结束了。从「为什么用 MQ」到「AMQP 模型」到「消费语义」到「可靠性」,这套认知是后面所有篇章的地基。阶段二会从模型层往功能层走,展开 RabbitMQ 最有特色的部分——路由设计。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。我相信 AI 是程序员的最佳搭档。想跟完这套「图解 RabbitMQ」,欢迎关注公众号 「十三Tech」

十三Tech公众号二维码