设备在线状态系统¶
单元 13|难度:中级|前置:MQTT、Redis、状态机|目标:把连接与心跳转化为可恢复的业务状态
真实场景¶
平台需要回答:
- 某台设备当前是否在线?
- 一个租户当前有多少在线设备?
- 设备何时离线?
- 列表中的 100 台设备分别是什么状态?
- 状态服务重启或 Redis 丢失后如何恢复?
“在线”不是数据库里一个永远正确的布尔值,而是系统根据连接、心跳和超时规则推导出的状态。
首先定义“在线”¶
常见定义:
- MQTT Broker 中存在活动连接;
- 最近 N 秒收到过心跳或遥测;
- 设备主动发送上线/离线事件;
- 上述信号组合。
必须明确业务语义。例如:
最近 90 秒内收到心跳,视为在线;超过 90 秒未收到,视为离线。
如果心跳周期为 30 秒,90 秒容忍两次左右的丢包或延迟。阈值不能凭感觉设置,要结合网络质量和业务对误判的容忍度。
基础数据流¶
为什么要区分事实和查询视图¶
last_seen、Broker 会话、状态迁移记录是事实或可追溯依据;online_devices、online_count是为了查询性能维护的视图;- 查询视图可以重建,因此即使统计漂移也有校准来源。
方案一:每设备 key + TTL¶
每次心跳刷新值和 TTL。key 不存在时,可认为设备已超时。
优点:
- 简单;
- 每台设备独立过期;
- 查询单设备快。
需要注意:
- Redis key 过期不是严格准时的业务调度器;
- Keyspace Notification 默认配置和可靠性不适合直接当作唯一离线事件来源;
- 只看 key 是否存在无法保存完整状态迁移历史;
- Redis 故障后需要等待设备重新上报或执行重建。
方案二:ZSet 保存离线截止时间¶
心跳到达时更新 score;后台 worker 持续查询截止时间小于当前时间的成员,进行离线状态迁移。
优点:
- 可以成批找出已超时设备;
- 离线判定流程更可控;
- 适合按租户或分片处理。
代价:
- 需要 worker 和并发控制;
- 必须避免旧任务把已重新上线的设备误判为离线;
- 大规模时需要分片,避免一个巨大热点 ZSet。
在线集合与计数¶
发生确认的状态迁移时:
查询在线数量:
Set 的去重性使重复上线事件相对安全。但它不能自动识别断电设备,仍然依赖超时检测。
如果使用 INCR/DECR 维护纯计数,必须只在真实状态迁移时更新,并定期从状态事实或集合校准。
批量查询一页设备状态¶
- 数据库分页读取 50~100 台设备基础信息;
- 提取本页 device_id;
- Redis 使用
MGET或HMGET批量读取; - 应用层按 device_id 合并;
- 返回这一页,不读取全量十万设备。
这样应用内存只短暂保存一页数据,通常是 KB 到低 MB 级,而不是把全量设备装入内存。
关键故障¶
重复心跳¶
心跳只刷新 last_seen,不应每次都生成“上线”事件。只有旧状态不是 online 时才发生状态迁移。
设备突然断电¶
不会发送离线事件,必须由超时检测产生离线状态。
离线后迅速重连¶
离线 worker 执行前要再次比较当前版本、last_seen 或 deadline,避免把已重连设备标为离线。
Redis 丢失¶
- 设备继续上报后逐步恢复;
- Broker 会话可以辅助重建;
- 数据库保留必要的状态迁移历史;
- 统计值是可重建视图,不把 Redis 当作唯一永久事实。
规模扩大时¶
十万设备、30 秒心跳的平均到达率约为:
这只是平均值。真实系统还要考虑同时重连、网络恢复后的突发流量和消息放大。
扩展方向:
- 状态服务按 tenant_id 或 device_id 分片;
- Redis Cluster 或多个独立状态分片;
- 心跳只更新必要字段;
- 状态迁移事件异步处理;
- 监控消息积压、处理延迟和状态漂移;
- 压测突发重连,而不只测均匀流量。
自测¶
- 为什么“收到心跳就 INCR”会统计错误?
- TTL key 已过期是否等于离线事件一定已经可靠执行?
- Set 为什么不能独立解决突然断电?
- 离线 worker 如何避免把刚刚重连的设备标成离线?
- Redis 丢失后,哪些数据能够重建,哪些应持久化?
完成标准¶
- 能定义“在线”的业务语义
- 能区分状态事实与查询视图
- 能比较 TTL 与 ZSet 超时调度
- 能处理重复心跳、突然断电和快速重连
- 完成 Redis 在线状态实验
下一单元:大量设备查询