MQTT的QoS0、QoS1、QoS2有什么区别?

MQTT 是物联网最常用的协议之一。很多人对它的印象是"轻量级的发布订阅",但真正把设备做稳定的团队,对 QoS 三个等级都有刻骨铭心的理解:消息丢了、重复了、恰好一次到底怎么保证,都是 QoS 的设计问题。本篇从原理出发,讲清三种 QoS 的语义、报文交互与适用场景,并用 mosquitto + paho-mqtt 做一个能亲眼看到"重复消息"的实操。

1 先回顾:MQTT 的发布订阅模型

图 5-1 发布订阅模型:Broker 按主题转发

MQTT 的所有通信都围绕"主题(Topic)"展开:发布者把消息发到某个主题,Broker 负责把主题匹配的消息转发给所有订阅者。发布者和订阅者互相不知道对方存在,Broker 是中间人。QoS(Quality of Service)定义的就是"发布者 → Broker → 订阅者"这条链路上,消息投递的可靠程度。

2 QoS 0:发完就不管,"最多一次"

QoS 0 的发布过程只有一问一答的一半:发布者发出 PUBLISH 报文后就当它消失了,Broker 不回复任何确认。语义上叫"最多一次(At most once)"------消息最多被投递一次,也可能零次(丢了就没了)。

适用场景:高频遥测数据,比如每秒上报的温度、GPS 坐标。丢一帧完全不影响大局,而每次确认的开销反而拖慢吞吐。QoS 0 的吞吐是三种里最高的,网络开销最小。

  • 优点:零确认开销、延迟最低、吞吐最高;
  • 缺点:网络抖动、Broker 重启、客户端掉线期间的消息全部丢失;
  • 典型坑:以为"连上了就一直在收",实际链路断了 5 秒,5 秒数据就没了。

3 QoS 1:至少一次,代价是可能重复

QoS 1 引入 PUBACK 确认:Broker 收到 PUBLISH 后返回 PUBACK,发布者收到 PUBACK 才算投递成功;如果发布者在超时时间内没收到 PUBACK,就会认为消息没送达而重发。这就保证了"至少一次(At least once)"------消息不会丢,但可能收到不止一次。

图 5-2 三种 QoS 的报文交互对比

重复发生的典型场景:网络丢包导致 PUBACK 丢失,发布者重发同一条消息;或者 Broker 已转发给订阅者但 PUBACK 丢了,Broker 侧重发。订阅者端收到两条一模一样的消息,业务层如果没有幂等处理,就会出现"一个订单下两遍""一条告警弹两次"。

图 5-4 QoS1 重复消息与幂等化解

  • 优点:可靠性好,适合命令下发、告警、计费类消息;
  • 缺点:订阅方必须做幂等,这是很多人忽略的隐性成本;
  • 实现要点:MQTT 3.1.1 中报文携带 PacketID,重发时 PacketID 不变,订阅端可以利用这一点做去重。

4 QoS 2:恰好一次,四段握手的代价

QoS 2 的目标是"恰好一次(Exactly once)",在 MQTT 协议里它是靠四段握手实现的:

图 5-3 QoS 2 的四段握手与重发逻辑

▍QoS 2 四段握手

发布者 → Broker:PUBLISH (带 PacketID)

Broker → 发布者:PUBREC (确认收到)

发布者 → Broker:PUBREL (请投递)

Broker → 发布者:PUBCOMP (投递完成)

关键点在 PUBREL:它本身就是一个"确认的确认"。如果 PUBREL 丢失,发布者会重发 PUBLISH(相同的 PacketID),Broker 认得这个 PacketID,不会重复投递,只重新发 PUBREC------于是发布者再次发出 PUBREL,最终到达 PUBCOMP。整个过程中订阅者只会收到一次消息。

  • 优点:理论语义上"恰好一次",订阅方无需幂等;
  • 缺点:四次报文交互、两个 RTT,延迟最高、吞吐最低;Broker 需要维护 PacketID 状态表,资源占用大;
  • 适用:只允许一次的业务,如"开锁指令""支付确认",且业务方不打算在应用层做幂等时。

5 三种 QoS 对比

|---------------|-----------|-----------|-----------------------|
| 维度 | QoS 0 | QoS 1 | QoS 2 |
| 语义 | 最多一次 | 至少一次 | 恰好一次 |
| 确认机制 | 无 | PUBACK | PUBREC/PUBREL/PUBCOMP |
| 丢包表现 | 消息丢失 | 不丢,但可能重复 | 不丢且不重复 |
| 报文开销 | 1 报文 | 2 报文 | 4 报文 |
| 延迟 | 最低 | 中 | 最高 |
| Broker 开销 | 最小 | 中 | 最大(状态表) |
| 订阅方要求 | 无 | 必须幂等 | 理论无要求 |
| 典型场景 | 高频遥测 | 命令下发/告警 | 开锁/支付等强一致 |

6 实操:用 mosquitto + paho-mqtt 亲眼看到"重复"

6.1 搭一个本地 Broker

▍mosquitto 安装与命令行自测

bash 复制代码
# Ubuntu/Debian 直接安装(或用 Docker: docker run -p 1883:1883 eclipse-mosquitto)
sudo apt install mosquitto mosquitto-clients

# 后台启动(开发机)
sudo systemctl enable --now mosquitto

# 快速自测
mosquitto_sub -h localhost -t "demo/test" -v -q 1 &
mosquitto_pub -h localhost -t "demo/test" -q 1 -m "hello"

6.2 Python 演示:订阅 QoS1 + 发布,观察重发

▍paho-mqtt:订阅 QoS1 并观察重复

python 复制代码
import paho.mqtt.client as mqtt
import time, json, threading


received = []


def on_message(c, u, msg):
    received.append(msg.payload.decode())
    print("收到:", msg.payload.decode(),
          "| qos:", msg.qos, "| duplicate:", msg.duplicate)
 

client = mqtt.Client("subscriber", protocol=mqtt.MQTTv5)
client.on_message = on_message
client.connect("localhost", 1883, 60)
client.subscribe("demo/order", qos=1)   # 订阅 QoS1 主题


pub = mqtt.Client("publisher", protocol=mqtt.MQTTv5)
pub.connect("localhost", 1883, 60)
 

# 发两条,模拟网络差时 Broker/客户端重发(duplicate=True 即重发报文)
for i in range(2):
    pub.publish("demo/order", "create-1001", qos=1)
    time.sleep(0.3)


time.sleep(1)
print("共收到", len(received), "条:", received)
# 输出中可能看到 duplicate=True ------ QoS1 的重复就长这样

6.3 用幂等化解 QoS1 的重复

最稳妥的做法是在业务入口处做幂等,而不是指望 QoS:

▍幂等处理:按业务唯一键去重

python 复制代码
import threading

 
_seen = set()
_lock = threading.Lock()
 

def on_order_message(msg_id, payload):
    with _lock:
        if msg_id in _seen:          # 重复投递 → 直接丢弃
            print("重复消息,丢弃:", msg_id)
            return
        _seen.add(msg_id)            # 生产环境用 Redis SET NX 代替
    create_order(payload)            # 业务真正执行一次


# 关键点:msg_id 用业务唯一键(订单号/指令号),
# 而不是 MQTT 的 PacketID------PacketID 只在一次会话内唯一

7 常见 Bug 与重难点

|-------------------|---------------------------------|-------------------------------|
| 问题 | 根因 | 对策 |
| 同一个订单下了两遍 | QoS1 重复投递,业务没做幂等 | 业务唯一键 + Redis 去重 |
| QoS2 指令偶尔没执行 | 重连后 PUBREL 未确认、会话状态丢失 | 确认 broker 会话保持;关键业务应用层补幂等 |
| 上线瞬间收到"陈年旧消息" | retained 消息残留 | 不需要的历史值用空消息清除 retained |
| 设备"神秘掉线" | keepalive 设置过短/网络抖动,broker 判定离线 | 调大 keepalive,检查网络稳定性 |
| 一条消息订阅到三份 | 主题通配符 #/+ 用错,误订阅了兄弟主题 | 缩小主题层级,显式订阅 |
| QoS2 延迟高被投诉 | 四段握手 RTT 多,弱网下尤其明显 | 低延迟链路用 QoS1 + 幂等,QoS2 留给强一致业务 |

【工程上最常用的组合是:遥测用 QoS0/1,指令下发用 QoS1 + 应用层幂等,QoS2 只留给"漏一次就是事故"的业务。把幂等做好,你甚至不需要 QoS2。】

【调试 MQTT 问题时用 `mosquitto_sub -v` 或 MQTTX 看原始报文,比翻代码快得多;`duplicate` 标志位是判断重发最直接的证据。】

8 小结

  • QoS0 最多一次:无确认、零开销,适合高频遥测,丢就丢了;
  • QoS1 至少一次:PUBACK 保证不丢,但必须接受重复,订阅方要做幂等;
  • QoS2 恰好一次:四段握手,语义最严谨但延迟最高、Broker 开销最大;
  • 可靠性 ≠ 复杂度:能用 QoS1 + 幂等解决的事,不要引入 QoS2;
  • 报文交互细节(PUBACK/PUBREC/PUBREL/PUBCOMP、PacketID)是排查重发/丢失问题的第一线索。
相关推荐
靠谱的工程师朋友1 小时前
电子会计档案管理软件科普 核心能力与选型要点解析
网络·数据库·人工智能·电子会计档案
买房动力十足1 小时前
秒杀商城 + AI 运营:Lua 防超卖、MQ 削峰,AI 负责选品和策划
开发语言·人工智能·lua
forest_091 小时前
《大模型原理通俗讲》系列 00|开篇 大模型到底是怎么回事
人工智能·python·prompt·agi
silvamux_1 小时前
10款模型写“君幸食”讲解词:统一事实档案与逐款审校
开发语言·php
北冥you鱼1 小时前
Go 语言 Ticker 定时任务全解析:原理、陷阱与最佳实践
开发语言·数据库·golang
A黄俊辉A1 小时前
虚拟机中的 桥接,NAT,仅主机模式的区别
网络·智能路由器
工程师迪菲1 小时前
【基础篇 01】YOLO 到底在做什么?先从一张图片看明白
python
牢姐与蒯1 小时前
Linux网络篇——TCP(一)之手搓TCP,并实现词典和远程命令
linux·网络·tcp/ip
晚风予卿云月1 小时前
【C++高并发内存池】 1.定长内存池的实现
开发语言·c++·内存管理