在物联网项目中,设备成功连接MQTT Broker,并不代表数据链路已经可靠。
实际运行一段时间后,经常会遇到这些问题:
-
同一条数据被平台收到两次;
-
设备显示在线,但部分消息没有上报;
-
网关断网后恢复连接,历史数据没有补传;
-
设备重连后突然收到大量旧消息;
-
控制指令已经过期,却在重连后被设备执行;
-
消息发布成功,但业务系统没有收到;
-
网络抖动时数据延迟明显增加。
这些现象通常不是单一故障造成的,而是与MQTT的QoS、会话状态、离线队列、保留消息和业务确认机制有关。
本文从实际工程角度梳理MQTT消息可靠性问题及常见处理方法。
一、MQTT的"发布成功"到底代表什么?
调用MQTT客户端的publish()方法后,很多开发者会认为消息已经成功写入业务系统。
实际上,一条设备消息通常要经过多层链路:
设备或边缘网关
↓
MQTT客户端发送队列
↓
网络连接
↓
MQTT Broker
↓
订阅客户端
↓
消息消费程序
↓
数据库或业务系统
MQTT协议主要负责客户端与Broker之间的消息传输。
即使Broker已经收到消息,也不能直接证明:
-
订阅客户端已经收到;
-
消费程序已经处理;
-
数据已经成功写入数据库;
-
告警规则已经执行;
-
下游业务系统没有报错。
因此,MQTT层面的确认与业务层面的成功,是两个不同概念。
对于普通遥测数据,确认Broker收到可能已经足够;对于远程控制、订单、告警等关键消息,还需要额外的业务回执机制。
二、QoS 0、QoS 1和QoS 2有什么区别?
MQTT提供三个消息服务质量等级。
1. QoS 0:最多发送一次
QoS 0的特点是发送方发送后不等待确认。
Publisher → PUBLISH → Broker
如果发送过程中出现网络中断,消息可能丢失。
它的优点是:
-
通信开销小;
-
延迟低;
-
适合高频数据;
-
不需要维护复杂状态。
适用场景包括:
-
每秒上报一次的温度;
-
高频设备运行状态;
-
允许少量数据缺失的环境监测;
-
可以被下一条数据覆盖的实时属性。
如果温度每5秒上报一次,偶尔丢失一条通常不会影响整体趋势,此时没有必要为每条消息增加额外确认开销。
2. QoS 1:至少到达一次
QoS 1要求接收方返回PUBACK确认。
Publisher → PUBLISH → Broker
Publisher ← PUBACK ← Broker
如果发布方在规定时间内没有收到确认,就会重新发送消息。
因此,QoS 1能够降低消息丢失概率,但可能产生重复消息。
常见情况是:
-
Broker已经收到并处理消息;
-
Broker返回
PUBACK; -
网络发生抖动,发布方没有收到确认;
-
发布方再次发送同一条消息;
-
Broker或下游系统再次处理。
所以QoS 1保证的是"至少到达一次",不是"只处理一次"。
适用场景包括:
-
设备告警;
-
设备属性上报;
-
工单状态变化;
-
重要但允许做幂等处理的数据。
3. QoS 2:只到达一次
QoS 2通过四次交互减少重复投递:
Publisher → PUBLISH → Broker
Publisher ← PUBREC ← Broker
Publisher → PUBREL → Broker
Publisher ← PUBCOMP ← Broker
QoS 2的协议开销最大,对客户端、Broker和网络资源的要求也更高。
它适合:
-
对重复极其敏感的少量关键消息;
-
无法通过业务幂等消除重复的场景;
-
消息数量不大但可靠性要求很高的业务。
在大规模物联网项目中,不建议将所有遥测数据都设置为QoS 2。否则容易增加连接负担、消息积压和Broker压力。
三、为什么使用QoS 1后仍然需要幂等处理?
QoS 1是物联网项目中最常见的选择,但必须接受一个事实:
消息可能重复。
因此,每条重要消息最好携带唯一标识。
{
"messageId": "gw01-1788489600123-0008",
"deviceId": "meter-001",
"timestamp": 1788489600123,
"type": "property",
"properties": {
"voltage": 220.5,
"current": 3.6
}
}
消费端收到消息后,可以检查messageId是否已经处理。
伪代码如下:
if (messageRepository.exists(messageId)) {
return;
}
processMessage(message);
messageRepository.save(messageId);
为了避免幂等记录无限增长,可以根据业务时效设置过期时间。
例如:
-
实时属性消息保留1小时的去重记录;
-
告警消息保留7天;
-
控制指令保留30天;
-
关键业务记录长期保存。
除了messageId,还可以使用以下字段组合生成幂等键:
deviceId + timestamp + messageType + sequence
但需要保证设备时间和序列号的可靠性。
四、设备断网后为什么没有收到离线消息?
MQTT是否保存离线消息,与多个条件有关:
-
客户端是否使用持久会话;
-
订阅是否已经建立;
-
消息QoS是否大于0;
-
Broker是否启用离线队列;
-
队列容量和消息有效期如何设置;
-
客户端是否使用固定Client ID。
只要其中一个条件不满足,断网期间的消息就可能无法恢复。
五、Clean Session与Clean Start有什么作用?
在MQTT 3.1.1中,常见参数是Clean Session。
Clean Session = true
客户端每次连接都创建新会话。
断开连接后,Broker通常会清除:
-
原有订阅关系;
-
未确认消息;
-
离线消息;
-
会话状态。
适合:
-
临时客户端;
-
调试工具;
-
不需要恢复历史消息的应用;
-
每次连接都会重新订阅的客户端。
Clean Session = false
Broker会保留客户端会话。
客户端重连后,可以恢复:
-
原有订阅;
-
部分未确认消息;
-
符合条件的离线消息。
在MQTT 5.0中,相关机制被拆分为Clean Start和Session Expiry Interval,能够更精细地控制会话生命周期。
例如:
Clean Start = false
Session Expiry Interval = 86400
表示尝试恢复已有会话,并在客户端离线后保留会话24小时。
需要注意:持久会话通常依赖稳定的Client ID。如果设备每次重连都生成一个新的Client ID,Broker会把它识别为新的客户端,原有离线消息也就无法恢复。
六、Client ID重复会产生什么问题?
MQTT Broker通常要求同一时刻一个Client ID只对应一个连接。
如果两个客户端使用相同Client ID:
-
客户端A连接Broker;
-
客户端B使用相同Client ID连接;
-
Broker断开客户端A;
-
客户端A自动重连;
-
Broker又断开客户端B;
-
两个客户端不断互相挤下线。
日志中可能看到频繁的连接和断开,但现场人员会误以为是网络不稳定。
因此,Client ID应具备全局唯一性,并与设备身份稳定绑定,例如:
park01-gateway-0001
park01-meter-0235
buildingA-edge-02
不建议仅使用随机数,也不要只使用可能重复的设备型号。
七、保留消息为什么会让设备收到"旧指令"?
MQTT的Retained Message用于保存某个主题的最后一条消息。
发布时设置Retain标志后,Broker会保存该消息。新的订阅者订阅主题时,会立即收到这条历史消息。
它适合保存:
-
设备最近状态;
-
当前配置版本;
-
在线状态;
-
当前模式;
-
最新属性快照。
但如果将远程控制指令设置为保留消息,就可能产生危险。
例如,平台向以下主题发布一条"启动设备"的保留消息:
park/device/pump-01/command/set
设备当时处于离线状态。数小时后设备重新上线并订阅该主题,Broker会立即把旧的启动指令发送给设备,导致过期指令被执行。
因此,控制指令通常不应该使用Retain。
如果需要清除某个主题的保留消息,可以向该主题发布一条空Payload并设置Retain标志。具体行为需要结合Broker实现验证。
八、遗嘱消息能否准确判断设备离线?
MQTT Last Will用于客户端异常断开时,由Broker代替客户端发布一条预设消息。
连接时可以配置:
Will Topic: park/device/gateway-01/status
Will Payload: offline
Will QoS: 1
Will Retain: true
客户端正常上线后主动发布:
Topic: park/device/gateway-01/status
Payload: online
Retain: true
这样,新订阅者能够获得设备最近的在线状态。
但遗嘱消息也不是绝对实时。
设备是否被判断为离线,还受到以下因素影响:
-
Keep Alive时间;
-
网络中间设备的连接超时;
-
Broker检测连接失效的速度;
-
客户端是否正常发送心跳;
-
TCP连接是否处于半开状态。
如果Keep Alive设置为60秒,平台通常不可能在设备断网后的1秒内准确判断离线。
在线状态判断应结合MQTT连接状态、最后数据时间和业务心跳,而不是只依赖一个字段。
九、断网缓存应该放在客户端还是Broker?
需要区分两个方向。
设备到云端的数据上报
设备或边缘网关断网时,消息还没有到达Broker。
这种情况下,Broker无法帮助缓存,必须由设备端或边缘网关保存数据。
常见实现方式包括:
-
内存队列;
-
本地文件;
-
SQLite;
-
嵌入式时序数据库;
-
轻量消息队列。
缓存数据至少需要包含:
{
"messageId": "gw01-000123",
"deviceId": "sensor-08",
"collectTime": 1788489600123,
"payload": {
"temperature": 25.6
},
"retryCount": 0
}
网络恢复后按照采集时间补传。
云端到设备的消息下发
设备离线时,控制指令已经到达Broker。
是否进入离线队列,取决于持久会话、QoS和Broker配置。
即使Broker支持离线消息,也应为控制指令设置有效期:
{
"commandId": "cmd-10086",
"createTime": 1788489600000,
"expireTime": 1788489660000,
"action": "start"
}
设备收到后首先检查:
currentTime > expireTime ?
如果指令已经过期,应拒绝执行并返回明确结果。
十、为什么消息会出现明显延迟?
MQTT消息延迟可能来自多个环节。
客户端发送队列积压
设备采集速度大于网络发送速度时,消息会在本地排队。
需要监测:
-
当前队列长度;
-
最老消息等待时间;
-
每秒产生消息数量;
-
每秒成功发送数量;
-
消息丢弃数量。
Broker性能不足
大量连接、通配符订阅、高QoS消息和持久会话都会增加Broker负担。
应关注:
-
当前连接数;
-
每秒消息吞吐;
-
消息队列长度;
-
磁盘写入延迟;
-
订阅匹配耗时;
-
节点之间的同步延迟。
消费端处理缓慢
MQTT消息已经到达订阅程序,但数据库写入、规则计算或外部接口调用耗时过长,也会造成业务延迟。
消费端应避免在MQTT回调线程中执行长时间操作。
不推荐:
public void messageArrived(String topic, MqttMessage message) {
callSlowHttpApi(message);
writeDatabase(message);
runComplexRule(message);
}
更合理的方式是快速接收后写入内部队列:
public void messageArrived(String topic, MqttMessage message) {
internalQueue.offer(new MessageTask(topic, message));
}
再由独立工作线程完成解析、存储和业务处理。
十一、如何设计关键控制指令?
对于远程开关、参数设置和设备复位等操作,不能只依赖MQTT QoS。
建议设计完整的请求与响应机制。
下发指令:
{
"commandId": "cmd-20260904-001",
"deviceId": "pump-01",
"action": "start",
"createTime": 1788489600000,
"expireTime": 1788489660000
}
设备执行后返回:
{
"commandId": "cmd-20260904-001",
"deviceId": "pump-01",
"result": "success",
"executeTime": 1788489601250,
"message": "device started"
}
平台根据commandId关联请求和结果,并维护状态:
待发送 → 已发送 → 设备已接收 → 执行成功
↘ 执行失败
↘ 执行超时
这样才能区分:
-
消息是否发出;
-
Broker是否收到;
-
设备是否收到;
-
指令是否执行;
-
执行是否成功。
十二、MQTT消息可靠性排查清单
当项目出现重复、丢失或延迟时,可以依次检查:
连接层
-
Client ID是否唯一且稳定;
-
是否频繁断线重连;
-
Keep Alive是否合理;
-
TLS握手是否失败;
-
网络是否存在高延迟和丢包。
会话层
-
Clean Session或Clean Start如何设置;
-
会话过期时间是否合理;
-
重连后是否重新订阅;
-
Broker是否保留离线队列。
消息层
-
QoS等级是否符合业务;
-
是否错误使用Retain;
-
消息是否包含唯一ID;
-
是否设置消息有效期;
-
Payload大小是否过大。
客户端
-
本地发送队列是否积压;
-
断网是否缓存;
-
重连策略是否采用退避机制;
-
重发时是否保持原始消息ID;
-
本地时间是否准确。
Broker
-
连接数和吞吐量是否达到瓶颈;
-
持久化是否正常;
-
队列长度是否超限;
-
消息是否因过期或策略限制被丢弃。
消费端
-
回调线程是否被阻塞;
-
是否存在重复消费;
-
数据库写入是否失败;
-
是否实现幂等处理;
-
下游接口是否超时。
总结
MQTT协议并不会自动解决物联网系统中的所有可靠性问题。
QoS解决的是客户端与Broker之间的消息交付等级;持久会话和离线队列解决的是特定条件下的消息恢复;Retain用于保存主题的最后状态;Last Will用于辅助判断异常断线。
真正稳定的物联网数据链路还需要:
稳定Client ID
+ 合理QoS
+ 持久会话
+ 端侧断网缓存
+ 消息唯一标识
+ 消费端幂等
+ 指令有效期
+ 业务结果回执
+ 全链路监控
对于周期性遥测数据,可以接受少量丢失;对于告警和控制指令,则必须补充业务层确认机制。
只有根据消息类型选择不同的可靠性策略,才能在系统性能、通信成本和数据完整性之间取得平衡。