首页 / 高并发 / 消息队列 / 投递语义 QoS
高并发 · 消息队列 · 投递语义

MQ 的 QoS 投递语义,一次讲清

最多一次、至少一次、精确一次——到底差在哪?为什么会有丢失和重复?
下面用可交互的故障模拟让你亲眼看到消息的去向。

1 先搞懂:QoS 到底在保什么 / What is guaranteed

MQ 在网络里跑,故障是常态:网络抖一下、Broker 宕机、消费者崩溃……消息随时可能「没送到位」。QoS(服务质量 / 投递语义)要回答一个问题:

一条消息,最终被消费者「成功处理」几次? 答案只可能是三种之一:0 次(丢了)1 次多于 1 次(重复了)。三种 QoS 就是在这三个数字上做取舍。
0 次 · 丢失 1 次 · 正好 ≥2 次 · 重复 最多一次 精确一次 至少一次
图 1:三种 QoS 就是在这条「投递次数」数轴上占据不同的区间。
记住这张图的对应关系:
最多一次 At-Most-Once 允许落在 0(可能丢),绝不会到 ≥2(绝不重复)。
至少一次 At-Least-Once 允许落在 ≥2(可能重复),绝不会到 0(绝不丢)。
精确一次 Exactly-Once 永远钉在 1,既不丢也不重复。

2 三种语义,点开看细节 / The three levels

点击任意一张卡片展开:它的机制、代价和适用场景。

最多一次

At-Most-Once · 可能丢失,绝不重复
  • 机制:发完就当成功,不重试;消费者先提交位移、后处理
  • 代价:任何环节故障 → 消息静默丢失,你根本不知道。
  • 适用:日志、指标、可容忍丢数据的埋点;丢了不影响业务。
  • 追求:低延迟、高吞吐,宁愿丢也不想重。

至少一次

At-Least-Once · 绝不丢失,可能重复
  • 机制:发送方超时重试直到确认;消费者先处理、后提交位移
  • 代价:故障下同一条消息被投递多次 → 业务需自己幂等。
  • 适用:绝大多数业务(订单、扣款前置),配合幂等最稳。
  • 现状:Kafka / RocketMQ 默认就是至少一次。

精确一次

Exactly-Once · 不丢不重,正好一次
  • 机制:幂等生产者 + 事务 + 幂等消费,三件套。
  • 代价:实现最复杂、性能开销最大,并非"免费"。
  • 适用:钱相关、不能重复也不能丢(转账、计费)。
  • 真相:主流是"至少一次 + 幂等",做不到理论真·一次。

3 一切差异,源于两个「时机」 / The root cause

为什么三种语义表现不同?拆开看,整条链路只有两个决策点决定消息会不会丢 / 重:

决策点 ①:生产者发失败,重试吗?
• 不重试 → 可能(最多一次)
• 重试 → 可能(至少一次)
• 重试 + 去重 → 正好(精确一次)
决策点 ②:消费者先提交位移,还是先处理?
• 先提交后处理 → 崩了就(最多一次)
• 先处理后提交 → 崩了重投就(至少一次)
• 处理与提交原子化 → 正好(精确一次)
一句话:「丢」来自不重试 / 早提交;「重」来自重试 / 晚提交。精确一次就是用幂等 + 事务把"重"消灭掉,同时保住"不丢"。

4 交互模拟器:亲眼看到消息去哪了 / Live simulator

左边选 QoS 模式故障场景,点「运行模拟」,看消息是丢失、重复还是正好一次。

① 选择 QoS 模式

② 选择故障场景

📤生产者
🗄️Broker
📥消费者
📨
就绪——选择模式与故障后点击运行
成功处理 故障点 重复副本

怎么读这个模拟:无论选哪种 QoS,无故障时结果都是"正好一次"——差异只在有故障/重试时才显现。重点对比:同样一次「消费者崩溃」,最多一次会、至少一次会、精确一次仍正好。这就是三者的本质区别。

5 精确一次,到底怎么实现的 / How exactly-once works

精确一次几乎从不靠"魔法",而是用至少一次打底 + 幂等去重把重复消掉。三件套:

① 幂等生产者

Producer ID + 序列号
  • Broker 给每个生产者发 PID,每条消息带序列号。
  • 重复副本到达时,Broker 比对序列号直接丢弃。
  • 消灭"发送重试导致的重复"。

② 事务消息

发送 + 存储 原子提交
  • 要么消息成功落盘并提交,要么整体回滚。
  • Broker 宕机不产出"半成品"。
  • 消灭"存储阶段的中断"。

③ 幂等消费

业务层去重兜底
  • 用唯一键(订单号等)判重:处理过就跳过。
  • 处理与提交位移放同一事务。
  • 消灭"重复投递造成的重复处理"。
精确一次  ≈  至少一次  +  幂等去重  +  事务
别被名字骗了: 真正的端到端"恰好一次"在分布式里极难(甚至不可能)严格保证。Kafka 的 exactly-once 是局限性下的最优解:它保证"在 Kafka 内部 + 幂等消费"范围内不丢不重,但跨系统(如消费后写 MySQL 又崩)仍需业务幂等兜底。

6 怎么选:一张表 + 口诀 / Choosing

维度最多一次至少一次精确一次
会丢失吗会 ❌不会 ✅不会 ✅
会重复吗不会 ✅会 ⚠️不会 ✅
复杂度最低中(需幂等)最高
性能最好较好最差
典型场景日志/埋点订单/通知(默认)转账/计费
选型口诀:
能容忍丢 → 最多一次(日志、监控)。
不能丢但要简单 → 至少一次 + 业务幂等(90% 系统的答案)。
钱相关、既不能丢也不能重 → 精确一次(幂等生产者 + 事务 + 幂等消费)。
终极心法: 别纠结"精确一次"这个名词。实际工程里,「至少一次 + 幂等消费」 就能解决 99% 的重复问题,且简单可靠。精确一次是为那 1% 强一致场景准备的"重武器"。