【MQTT】 QoS 详解:QoS0/1/2 的送达语义、QoS1 vs QoS2 取舍与最佳实践
MQTT协议通过QoS等级提供不同级别的消息可靠性保证:QoS0(最多一次)可能丢失但不重传;QoS1(至少一次)保证送达但可能重复;QoS2(恰好一次)通过四步握手避免重复交付。实际工程中,QoS1配合业务层幂等处理是最常用方案,因其平衡了可靠性和性能开销。QoS2适用于重复代价极高的场景,如控制指令。此外,retained消息和持久会话分别解决新订阅者获取最新状态和离线消息补发问题。建议根据业
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):
-
PUBLISH(QoS2, ID)
发布者发送消息,带 Packet Identifier(ID)。 -
PUBREC(ID)
订阅者收到后,记录“已收到该 ID 的 QoS2 消息”,回 PUBREC。 -
PUBREL(ID)
发布者收到 PUBREC,发送 PUBREL 表示进入释放阶段。 -
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:解决“新订阅者立即拿到最新状态”
- 持久会话:解决“离线期间补消息”
更多推荐



所有评论(0)