MQTT QoS 详解:QoS0/1/2 的送达语义、QoS1 vs QoS2 取舍与最佳实践

MQTT 是物联网与实时消息系统里非常常见的协议。它最关键的特性之一是 QoS(Quality of Service)服务质量等级:用不同的协议开销换取不同的送达保证。

很多工程问题都绕不开这些点:

  • QoS0/1/2 各代表什么语义?
  • QoS2 为什么能 “exactly once”?
  • 既然业务层可以用唯一键去重,QoS2 还有必要吗?
  • retained、持久会话是什么,跟 QoS 如何搭配?

这篇文章把这些问题一次讲透,并给出可落地的选型建议。


1. QoS 三种等级:你到底在保证什么?

QoS 0:At most once(最多一次)

  • 不保证送达,也不重传
  • 可能丢消息
  • 协议开销最小、延迟最低
  • 适用:高频遥测/不关键的状态上报(允许少量丢失)

QoS 1:At least once(至少一次)

  • 保证最终会送达(在连接可恢复/会话允许的前提下)
  • 协议流程:PUBLISH(QoS1)PUBACK
  • 如果发布端没收到 PUBACK,会重发 PUBLISH(通常带 DUP 标记)
  • 可能重复送达(重点)
  • 适用:不能丢但可接受重复的消息,比如告警、业务事件(配合幂等)

QoS 2:Exactly once(恰好一次)

  • 保证“协议层交付语义”恰好一次
  • 使用更复杂的握手与状态机避免重复交付
  • 开销最大,吞吐/延迟最差
  • 适用:重复代价极高,且希望尽量避免重复进入业务层的场景

2. QoS2 如何实现 exactly once?(四步握手 + 状态机)

QoS2 的核心是:双方都维护一个消息状态机(通过 Packet Identifier 标识),并且分阶段确认。即便网络重传导致报文重复出现,也不会导致消息被重复交付给应用层。

典型流程(发布者 P → 订阅者 S):

  1. PUBLISH(QoS2, ID)
    发布者发送消息,带 Packet Identifier(ID)。

  2. PUBREC(ID)
    订阅者收到后,记录“已收到该 ID 的 QoS2 消息”,回 PUBREC。

  3. PUBREL(ID)
    发布者收到 PUBREC,发送 PUBREL 表示进入释放阶段。

  4. PUBCOMP(ID)
    订阅者收到 PUBREL 后,完成最终交付并回 PUBCOMP,双方清理该 ID 状态。

为什么这能避免重复交付?

关键点在“重复报文不会触发重复交付”:

  • 如果订阅者重复收到 PUBLISH(QoS2, ID),它只会重复回 PUBREC(ID),不会再次交付业务。
  • 如果订阅者重复收到 PUBREL(ID),它会确保最多交付一次,之后只回 PUBCOMP(ID)

因此 QoS2 的 “exactly once”更准确的表达是:

在 MQTT 协议的交付语义中,同一条 QoS2 消息不会被重复交付给上层应用。

注意:这并不等于你跨系统的业务副作用天然就“严格一次”(例如写 DB、调 HTTP、发短信等仍建议幂等)。


3. QoS1 vs QoS2:核心区别是什么?

维度 QoS1 QoS2
语义 至少一次(可能重复) 恰好一次(协议层避免重复交付)
报文交互 2步:PUBLISH/PUBACK 4步:PUBLISH/PUBREC/PUBREL/PUBCOMP
资源与复杂度 更轻 更重(更多状态)
业务要求 业务侧幂等/去重很重要 业务侧仍建议幂等,但重复更少进入业务

一句话总结:
QoS1:可靠送达但可能重复;QoS2:可靠送达且尽量不重复交付,但代价更大。


4. 为什么工程上通常“优先 QoS1 + 幂等”?(性价比最高)

很多团队最终都会落在 “QoS1 + 业务幂等/唯一键”这个组合上,原因很现实:

4.1 QoS2 协议开销明显更大

QoS1 两步握手,QoS2 四步握手。
在弱网/高并发场景下,QoS2 会带来:

  • 更高 RTT 与延迟
  • 更低吞吐
  • broker/客户端更高的状态维护成本

4.2 QoS2 状态更复杂、问题更难排查

QoS2 需要双方维护“未完成事务”的状态机,断线重连、存储、清理都更复杂,排查也更困难。

4.3 大多数业务“不怕重复,只怕丢”

现实中更多业务是:

  • 允许重复(只要最终只生效一次)
  • 但不允许丢(事件必须到达)

因此更常见做法是:

  • QoS1 解决“最终能到达”
  • 业务幂等解决“重复不产生副作用”

这是成本与收益最平衡的一条路。


5. 那 QoS2 什么时候真的有必要?

你提出一个非常常见也很正确的问题:

我用 QoS1,再通过业务唯一键防重复,不就实现 QoS2 的效果了吗?

从“最终业务效果”角度,你确实可以做到“最终只生效一次”。
但 QoS2 仍有一些不可替代的优势场景:

场景 A:你无法保证所有消费者都正确幂等

比如嵌入式设备/第三方系统/历史系统,幂等能力弱或不可控。QoS2 把去重下沉到协议层,降低风险。

场景 B:重复的代价非常高且难回滚

例如:

  • 控制指令(重复执行可能危险)
  • 外部副作用(发短信、开门、点火、机械动作)
  • 一次性流程推进(重复会破坏状态机)

这类场景中你当然也能做幂等,但“幂等漏做/做错”的代价极高,QoS2 能把重复挡在业务层之外,显著降低事故概率。

场景 C:你希望业务层尽量“看不到重复”

QoS1 的重复会直接暴露给业务层,业务必须承受查重/锁/事务成本;QoS2 让重复更多停留在协议层。


6. 两个常被一起问的能力:retained 与持久会话

QoS 决定的是“送达语义”,但要处理“新订阅者如何拿到最新状态”“离线后是否要补消息”,还得看 retained 和持久会话。

6.1 retained message(保留消息)

  • 发布时设置 retain=true
  • broker 保存该 topic 的最后一条 retained 消息
  • 新订阅者订阅后立刻收到这条“最新状态快照”

适用:设备状态、开关当前值、最新配置、最近一次温度等“状态类”数据
不适用:需要保存每条历史事件的“事件流”(retained 只保留最后一条)

6.2 持久会话(Persistent Session / Durable Session)

让 broker 记住你的订阅关系,并在你离线时缓存 QoS1/2 的消息,等你上线补发。

  • MQTT 3.1.1:cleanSession=false
  • MQTT 5:clean start=false + session expiry interval > 0

适用:客户端经常掉线/休眠,但你希望它上线后补齐关键消息(告警、指令、关键事件)
注意:通常需要固定 clientId 才能恢复会话。


7. 实用选型建议(直接照着用)

7.1 高频遥测(数据量大、允许丢)

  • QoS0
  • 不开持久会话(避免离线积压)
  • 可选:如果希望新订阅者看到最新值,状态主题用 retained

7.2 业务事件/告警(不能丢,可接受重复)

  • QoS1 + 业务幂等(唯一键/幂等处理)
  • 如果订阅者会离线:开持久会话(让离线期间补消息)

7.3 控制指令/危险操作(不能丢且尽量不能重复)

  • 优先考虑 QoS2(降低重复进入业务层的概率)
  • 即便 QoS2,仍建议保留业务幂等作为兜底

结语

  • QoS0:轻量但可能丢
  • QoS1:可靠但可能重复(最常用)
  • QoS2:协议层避免重复交付,但代价更大(少数场景使用)
  • retained:解决“新订阅者立即拿到最新状态”
  • 持久会话:解决“离线期间补消息”
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐