4、遗嘱消息应用场景:设备离线通知、状态重置、分布式锁的异常释放
好,咱们接着聊遗嘱消息。前面几章我们把遗嘱消息的基本原理和配置讲透了,这一章我重点说说实际项目中怎么用。说实话,遗嘱消息这玩意儿,光看文档觉得挺简单,但真正落地的时候,坑不少。
我个人习惯把遗嘱消息的应用场景分成三类:设备离线通知、状态重置、分布式锁的异常释放。这三个场景基本覆盖了90%以上的实际需求。咱们一个一个来拆解。
4.1 设备离线通知
这是最直观的场景。你想想看,一个物联网系统里,成百上千的设备在线。突然某个设备掉线了,你怎么知道?
有人会说:「用心跳检测啊!」没错,心跳检测是一种方式。但心跳有延迟,而且如果设备是异常断电、网络瞬间断开,心跳包根本发不出去。这时候遗嘱消息就派上用场了。
我在项目中遇到过这样的情况:一个智能水表项目,设备装在井下,信号时好时坏。设备正常离线时会发一个clean shutdown的消息,但有时候电池耗尽或者被人为破坏,直接就断了。这时候遗嘱消息就能告诉我们:「这个设备非正常离线了」。
具体怎么做?很简单:
# 设备连接时设置遗嘱消息
client.will_set(
topic="device/{device_id}/status",
payload="offline",
qos=1,
retain=True
)
# 正常离线时手动清除遗嘱
client.publish("device/{device_id}/status", "offline_clean", qos=1, retain=True)
client.disconnect()
这里有个关键点:遗嘱消息和正常离线消息要区分开。我习惯用不同的payload值来标识:
| 状态值 | 含义 | 触发方式 |
|---|---|---|
| online | 设备在线 | 设备连接后发布 |
| offline | 异常离线 | 遗嘱消息自动触发 |
| offline_clean | 正常离线 | 设备主动发布 |
4.2 状态重置
这个场景可能很多人没想到。什么叫状态重置?说白了就是:设备离线了,它之前控制的一些状态需要自动恢复成默认值。
举个例子:智能灯控系统。一个灯被远程打开了,如果设备突然离线,这灯就一直亮着?显然不合理。我们希望设备离线后,灯自动关闭。
嗯,这里要注意:遗嘱消息不能直接控制硬件。它只能发布一条消息。真正的控制逻辑在订阅端实现。
我的做法是这样的:
# 设备端设置遗嘱消息
client.will_set(
topic="device/{device_id}/command/reset",
payload='{"action": "reset_all", "timestamp": 0}',
qos=2,
retain=False
)
# 订阅端(比如边缘网关)处理逻辑
def on_message(client, userdata, msg):
if "reset" in msg.topic:
# 解析遗嘱消息
payload = json.loads(msg.payload)
if payload["action"] == "reset_all":
# 执行状态重置
reset_device_states(msg.topic.split("/")[1])
# 记录日志
logger.warning(f"设备 {device_id} 离线,已执行状态重置")
你可能会问:「为什么不用保留消息?」问得好。保留消息是给新订阅者用的,而遗嘱消息是给当前订阅者用的。两者配合使用效果更好。
4.3 分布式锁的异常释放
这个场景比较高级,一般做物联网平台架构的人才会遇到。分布式锁在微服务架构中很常见,但用在物联网场景下,有个特殊问题:持有锁的设备挂了怎么办?
我记得有一次做车联网项目,多辆车同时竞争一个充电桩。我们用Redis分布式锁来控制。结果一辆车在充电过程中网络断了,锁一直没释放,其他车干等着。后来我改用MQTT遗嘱消息来解决这个问题。
具体方案是这样的:
# 申请锁时设置遗嘱消息
def acquire_lock(device_id, resource_id):
# 先设置遗嘱消息
client.will_set(
topic=f"lock/{resource_id}/release",
payload=device_id,
qos=2,
retain=True
)
# 再尝试获取锁
result = redis.setnx(f"lock:{resource_id}", device_id)
if result:
# 获取锁成功
return True
else:
# 获取锁失败,清除遗嘱消息
client.will_clear()
return False
# 释放锁时清除遗嘱消息
def release_lock(device_id, resource_id):
# 正常释放锁
redis.delete(f"lock:{resource_id}")
# 清除遗嘱消息
client.will_clear()
# 发布释放消息
client.publish(f"lock/{resource_id}/release", "", qos=2, retain=True)
这个方案的好处是:不需要额外的超时检测机制。设备一旦异常断开,Broker自动发布遗嘱消息,其他设备收到后就知道锁被释放了,可以重新竞争。
我曾经踩过一个坑:遗嘱消息的QoS设置成了0。结果网络抖动的时候,遗嘱消息丢了,锁一直没释放。后来我统一改成QoS 2,虽然性能有点损耗,但可靠性上去了。
4.4 三个场景的对比总结
说了这么多,咱们用一张表总结一下:
| 场景 | 核心用途 | 遗嘱消息内容 | QoS建议 | 是否保留 |
|---|---|---|---|---|
| 设备离线通知 | 通知系统设备异常离线 | 设备ID + 离线状态 | 1 | 建议开启 |
| 状态重置 | 触发设备状态恢复默认 | 重置指令 + 参数 | 2 | 视情况而定 |
| 分布式锁释放 | 自动释放异常持有的锁 | 锁ID + 持有者ID | 2 | 必须开启 |
最后说一句:遗嘱消息是个好东西,但别滥用。每个连接只能设置一个遗嘱消息,而且遗嘱消息的payload大小有限制(一般是256KB以内)。设计系统的时候,要想清楚哪个场景最需要遗嘱消息来兜底。
下一章我们聊聊遗嘱消息和保留消息的配合使用,这两个功能组合起来,能玩出很多花样。