前两篇已经把 order.created 的发布端和消费端代码搭起来了,但还有一个最容易被误解的边界没有单独讲透:发布者究竟什么时候才能说“这条消息已经可靠交给 RabbitMQ 了”? 本篇把 mandatory、returned messages、publisher confirms、RabbitMQ.Client 的确认跟踪,以及网络中断产生的结果未知窗口拆开说明。

本文不讨论消费者优雅关闭、manual ack、prefetch,也不回到 Outbox 或幂等实现。这里只收束发布端到 Broker 之间的可靠性语义。

系列导航:

统一示例:继续发布 order.created

仍然沿用前两篇完全相同的事件:

{
  "eventId": "01J7X8A6YQ4Q9M6Q4QG7H2J8F1",
  "eventType": "order.created",
  "occurredAt": "2026-07-15T10:00:00Z",
  "orderId": "ORD-20260715-001",
  "customerId": "C10086",
  "amount": 199.00,
  "currency": "CNY"
}

它通过 Topic Exchange orders.events 发布,路由键为 order.created。如果至少有一个 Queue 匹配这条路由规则,消息就是“可路由”的;如果一个都匹配不到,它就是不可路由消息。

mandatory:不要让不可路由消息静默丢失

AMQP 0-9-1 的常见默认行为是:发布到某个 Exchange 的消息,如果没有任何 Queue 匹配成功,Broker 可以直接丢弃它。发布者如果什么都没额外设置,往往不会立刻知道这次路由失败。RabbitMQ 的发布者指南把这种情况称为 unroutable message。

mandatory: true 就是为了解决这个盲点。它的语义非常直接:

  • 如果消息至少路由到一个 Queue,发布流程按正常路径继续。
  • 如果消息一个 Queue 都路由不到,Broker 不要静默吞掉,而是把它退回给发布者。

.NET 客户端里,可以先订阅 BasicReturnAsync,再使用 mandatory: true

using System.Text.Json;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

channel.BasicReturnAsync += (_, args) =>
{
    Console.Error.WriteLine(
        $"Returned: replyCode={args.ReplyCode}, replyText={args.ReplyText}, routingKey={args.RoutingKey}");

    return Task.CompletedTask;
};

var orderCreated = new
{
    eventId = "01J7X8A6YQ4Q9M6Q4QG7H2J8F1",
    eventType = "order.created",
    occurredAt = DateTimeOffset.Parse("2026-07-15T10:00:00Z"),
    orderId = "ORD-20260715-001",
    customerId = "C10086",
    amount = 199.00m,
    currency = "CNY"
};

ReadOnlyMemory<byte> body = JsonSerializer.SerializeToUtf8Bytes(orderCreated);

await channel.BasicPublishAsync(
    exchange: "orders.events",
    routingKey: "order.created",
    mandatory: true,
    body: body,
    cancellationToken: cancellationToken);

如果这时拓扑里没有任何 Binding 能匹配 order.created,发布者就能通过 return 观察到这次失败,而不是等业务方几小时后才发现“为什么库存和通知都没收到”。

但这里有一个容易被忽略的限制:mandatory 只检查有没有路由到至少一个 Queue,不检查“所有预期订阅者是否都具备正确 Binding”。假设库存 Queue 匹配成功、通知 Queue 的 Binding 丢失,消息仍然算可路由,不会 return。因此,多订阅者场景还需要拓扑声明、启动检查和监控共同保证完整性。

如果 Exchange 配置了 Alternate Exchange,消息经 Alternate Exchange 最终进入 Queue,也算成功路由,同样不会触发 return。官方的Alternate Exchange 说明明确指出,它会参与 mandatory 的路由判断。

returned messages 解决的是“路由失败可观察”,不是“业务失败可观察”

BasicReturnAsync 能告诉你的是:Broker 没能把这条消息路由到任何 Queue。 它解决的是“不可路由”这个问题,不是所有发布失败的总开关。

最容易混淆的地方有三个:

  • 返回了 returned message,不代表 Broker 崩了;它可能只是配置上没有匹配到任何 Queue。
  • 没有 returned message,不代表消费者已经处理成功;它只说明消息至少进入了某个 Queue 的路由结果。
  • returned message 也不等于 publisher nack。不可路由消息在协议层仍会收到 publisher confirm;使用 mandatory 时,RabbitMQ 会先发送 basic.return,再发送 basic.ack

所以更准确的说法是:

  • mandatory + returned message:解决“有没有路由到至少一个 Queue”,但不能证明所有预期 Queue 都存在。
  • publisher confirm:说明消息已按实际路由结果完成 Broker 侧接收;它本身不能证明消息一定可路由。
  • manual ack:解决“消费者是否声明这次投递已经处理完成”。

它们是三段不同边界,不能用一句“消息发成功了”替代。

publisher confirms:实际路由结果被 Broker 接受的证据

发布者最常见的误判是:在没有启用 confirmation tracking 的 Channel 上,看到 BasicPublishAsync() 返回,就以为 RabbitMQ 一定接管了这条消息。此时调用完成只能说明客户端的发送操作已经结束,不能证明 Broker 已按目标 Queue 的语义接受消息。

publisher confirms 就是为这个边界设计的。创建 Channel 时启用确认和客户端跟踪:

var channelOptions = new CreateChannelOptions(
    publisherConfirmationsEnabled: true,
    publisherConfirmationTrackingEnabled: true);

await using IChannel channel =
    await connection.CreateChannelAsync(channelOptions, cancellationToken);

然后正常发布:

var properties = new BasicProperties
{
    ContentType = "application/json",
    DeliveryMode = DeliveryModes.Persistent,
    MessageId = orderCreated.eventId,
    Type = orderCreated.eventType,
    Timestamp = new AmqpTimestamp(orderCreated.occurredAt.ToUnixTimeSeconds())
};

await channel.BasicPublishAsync(
    exchange: "orders.events",
    routingKey: "order.created",
    mandatory: true,
    basicProperties: properties,
    body: body,
    cancellationToken: cancellationToken);

仓库示例固定使用 RabbitMQ.Client 7.2.1。按照 CreateChannelOptionsBasicPublishAsync 的语义,同时启用 confirmation 和 tracking 后,BasicPublishAsync() 会等待这一次发布的确认结果:

  • 消息获得 publisher confirm,且没有被 mandatory return,则调用正常完成。
  • Broker nack 或 mandatory return 会抛出 PublishException;后者的 IsReturntrue
  • 等待期间超时、连接中断或 Channel 关闭,也会以异常结束,但结果不一定都是“Broker 没收到”。

BasicReturnAsync 仍然会先被触发,适合记录 reply code、routing key 和被退回的消息;同一次 return 随后还会让正在等待的 BasicPublishAsync() 抛出异常。应用应把事件当作观测入口,把调用异常当作当前发布任务的控制流,避免把同一次 return 重复记成两次失败。

这一结论有明确前提:confirmation tracking 已启用。 如果只启用协议层 confirms、没有启用客户端 tracking,BasicPublishAsync() 不会替你把每次调用与对应 confirm 关联起来,不能套用上述“正常返回即获得确认”的判断。

RabbitMQ 对 confirm 的准确承诺取决于实际路由结果和 Queue 语义。官方的publisher confirms 指南给出的边界是:

  • 不可路由消息:Exchange 确认无法路由后仍会发送 basic.ack;设置 mandatory 时,先 return,再 ack。
  • 可路由消息:消息被所有实际路由到的 Queue 接受后发送 basic.ack
  • persistent 消息进入 durable Queue:确认通常要等消息完成持久化。
  • 进入 Quorum Queue:确认要等法定数量副本接受消息。

所以,“Broker accepted”只是便于交流的简称,不能脱离 Queue 类型、消息持久化属性和 mandatory 结果单独理解。

并发模型不会改变发布结果的边界

SemaphoreSlim、多个独占 IChannel 和进程内 Channel<T> 解决的是“发布任务如何安全地到达 RabbitMQ 客户端”或“请求如何排队”,不是“Broker 是否已经接管”。它们和 publisher confirms 是两个正交的问题:

观察到的结果最多能证明什么仍然不能证明什么
写入 Channel<T> 成功当前进程接受了待发布任务RabbitMQ 已收到;进程重启前不会丢
开启 confirmation tracking 后,BasicPublishAsync 正常完成实际路由到的 Queue 已按自身语义接受消息,且没有 mandatory return所有预期 Queue 都存在;消费者已完成业务
本地事务写入 Outbox 成功事件进入了可恢复的待发布集合RabbitMQ 已接管消息

所以,单后台 Publisher 能避免多个请求同时使用一个 IChannel,但它不能把“进了内存队列”升级为“发布成功”;Channel Pool 能提高并行度,也不能缩小确认尚未返回时的结果未知窗口。共享 Channel 的正确并发模型见.NET 发布端实战(七),数据库写入 Outbox 后如何继续可靠发布见端到端可靠性(十一)

发布确认不等于业务完成

收到 publisher confirm,说明 RabbitMQ 已按实际路由结果完成 Broker 侧接收,但它仍然不能证明消费者完成了业务,也不能证明所有设计上预期存在的 Queue 都正确绑定。更清晰的分层是:

阶段机制最多能证明什么仍然不能证明什么
路由覆盖mandatory + return至少有一个 Queue 接收路由,或不可路由被明确发现所有预期 Queue 都配置正确
Broker 侧接收publisher confirms所有实际路由到的 Queue 已按自身语义接受消息消费者已经完成业务
Broker -> Consumer 处理完成manual ack消费者声明投递处理成功业务外部副作用绝对没有重复

回到 order.created

  • mandatory + tracking 下的发布调用正常完成,说明消息没有被 return,且实际路由到的 Queue 已接受消息。
  • 它仍然不能发现“库存 Queue 正常、通知 Queue 的 Binding 丢失”这种部分拓扑错误。
  • 库存服务是否真的预留成功,不在 confirm 的证明范围内。
  • 通知服务是否真的发出短信,也不在 confirm 的证明范围内。

因此,在业务语言里,把“消息已被 Broker 接收”描述成“订单创建流程已完成”是错误的。Broker accepted 是基础设施层成功,business completed 是业务层成功,两者之间还隔着消费与业务执行。

unknown-result window:网络在错误时间断开时,结果会变得不确定

真正棘手的场景不是“明确成功”或“明确失败”,而是结果未知。围绕 order.created,可能出现两条在发布者看来无法区分的路径:

  1. RabbitMQ 已经让目标 Queue 接受消息,并发出了 confirm,但响应在返回发布者前遇到网络中断。
  2. 发布帧还没有完整到达 RabbitMQ,连接就已经中断,Broker 实际上没有接收消息。

发布者看到的都可能只是超时或连接异常,却无法仅凭这个异常还原 Broker 里的真实结果。这就是发布端最关键的 unknown-result window:网络和进程故障把“真实结果”和“调用方观察结果”切开了。

对发布者而言,这时只能确定两件事:

  • 不能证明 Broker 没收到。
  • 也不能证明 Broker 一定收到了。

于是系统必须在“不确定”里做选择。最常见的策略是:

  • 记录这次发布的稳定 MessageId,例如 eventId
  • 把这次结果标记为 unknown,而不是误记成 definite failure。
  • 后续若选择重发,必须重发同一个 MessageId,并让消费端准备好处理重复。
发布结果的证据边界发布者发送消息后,不可路由消息会先被 return 再获得协议确认;可路由消息由实际目标队列接受后获得确认。响应丢失会形成未知窗口,消费者业务完成则属于下游边界。PUBLISH RESULT · EVIDENCE HAS FOUR DISTINCT LAYERSPublisherMessageId = evt_…Exchange / Queuerouting 的事实层Broker确认实际路由结果Consumer业务侧UNROUTABLE APPLICATION OUTCOMENO QUEUE MATCHED · DEFINITE RETURNbasic.return → PublishException随后仍有 protocol ackROUTABLE APPLICATION OUTCOMEONE OR MORE QUEUES MATCHEDbasic.ack → routed Queues acceptedRESULT UNKNOWN AFTER INTERRUPTIONCONNECTION LOST BEFORE OBSERVED RESULTunknown-result window → MessageId 不变后重发DOWNSTREAM BUSINESS BOUNDARYCONSUMER SIDE FACTbusiness completed(confirm 无法证明)不可路由时 basic.return 先于 basic.ack;tracking 将它呈现为发布异常,而不是成功结果。发布结果的证据边界移动布局按发送、路由、应用发布结果、网络中断和消费者业务完成展示不同证据边界,并说明不可路由消息仍会获得协议确认。EVIDENCE BOUNDARIESPublisher 发送 MessageIdExchange / Queue 路由先出现发送和路由UNROUTABLE RESULTbasic.return → PublishException随后仍会发送 protocol basic.ackROUTABLE RESULTbasic.ack → Queues accepted只覆盖实际路由,不证明预期 Queue 完整NETWORK INTERRUPTIONunknown-result windowMessageId 不变后重发BUSINESS COMPLETIONbusiness completedconfirm 无法证明的下游事实return 先于 protocol acktracking 将其归为发布异常
发布结果的证据边界使用 mandatory 与客户端确认跟踪时,应区分明确 return、正常确认、结果未知和业务完成;protocol confirm 本身不能证明消息一定可路由。
mandatory return 证明消息没有路由到任何队列,但协议层随后仍会确认这次发布;可路由消息的 confirm 表示所有实际路由到的队列已经接受消息。消费者业务完成仍是更下游的独立事实。

也就是说,unknown-result window 直接把系统推回 at-least-once 现实:要避免丢失,就必须接受可能重复。

按结果分类,而不是统一标记成“发送失败”

发布代码最终需要把观察结果分成可行动的类别:

观察结果当前能确定什么建议处理
BasicPublishAsync() 正常完成tracking 已启用时,消息获得 confirm 且没有 return记录发布成功;仍不要声称业务完成
PublishException.IsReturn == true消息明确不可路由记录 Exchange、routing key 与 reply code;修复拓扑或路由数据,不要盲目原路重试
其他 PublishExceptionBroker 对该发布给出 nack记录为明确的 Broker 侧拒绝,结合 Queue 限制和 Broker 状态决定是否重试
发布到不存在的 Exchange,Channel 被关闭目标 Exchange 或拓扑声明有误重建 Channel 前先修复拓扑;原 Channel 不能继续使用
confirm 超时、连接中断、进程退出可能已接收,也可能未接收标记为 unknown;保留相同 MessageId 重发,并依赖消费端幂等收敛重复
写入进程内 Channel<T> 成功任务只进入本进程内存队列不得记为 RabbitMQ 发布成功

围绕 order.created,发布者至少应做到:为消息设置稳定 MessageId;在业务要求“至少有一个接收队列”时使用 mandatory: true;启用 publisher confirms 与客户端 tracking;区分 return、nack、拓扑错误和 unknown,而不是把所有异常都归为同一种失败。RabbitMQ 的可靠性指南同样指出,连接故障后重发未确认消息可能造成重复,因此消费端必须去重或保持幂等。

本篇解决了什么:

  • 区分了 mandatory、returned messages、协议层 confirm 与客户端 tracking 各自解决的发布边界,明确 confirm 本身不能证明可路由或业务完成。
  • 说明了安全并发、进程内排队与发布确认是独立问题:前两者不能替代 Broker 接管证据。
  • 解释了 return、nack、拓扑错误和 unknown 应如何分类,以及发布端为何要用稳定 MessageId 面对可能重发与重复。

下一篇:RabbitMQ 消费可靠性:幂等、重试与死信队列(十)