跳转至

设备在线状态系统

单元 13|难度:中级|前置:MQTT、Redis、状态机|目标:把连接与心跳转化为可恢复的业务状态

真实场景

平台需要回答:

  • 某台设备当前是否在线?
  • 一个租户当前有多少在线设备?
  • 设备何时离线?
  • 列表中的 100 台设备分别是什么状态?
  • 状态服务重启或 Redis 丢失后如何恢复?

“在线”不是数据库里一个永远正确的布尔值,而是系统根据连接、心跳和超时规则推导出的状态。

首先定义“在线”

常见定义:

  1. MQTT Broker 中存在活动连接;
  2. 最近 N 秒收到过心跳或遥测;
  3. 设备主动发送上线/离线事件;
  4. 上述信号组合。

必须明确业务语义。例如:

最近 90 秒内收到心跳,视为在线;超过 90 秒未收到,视为离线。

如果心跳周期为 30 秒,90 秒容忍两次左右的丢包或延迟。阈值不能凭感觉设置,要结合网络质量和业务对误判的容忍度。

基础数据流

设备
  → MQTT Broker
  → 状态服务
  → 更新 last_seen / 过期调度
  → 发生状态迁移时发布 Online/Offline 事件
  → Redis 查询视图 + 数据库历史记录

为什么要区分事实和查询视图

  • last_seen、Broker 会话、状态迁移记录是事实或可追溯依据;
  • online_devicesonline_count 是为了查询性能维护的视图;
  • 查询视图可以重建,因此即使统计漂移也有校准来源。

方案一:每设备 key + TTL

SET device:last_seen:10001 1753761600 EX 90

每次心跳刷新值和 TTL。key 不存在时,可认为设备已超时。

优点:

  • 简单;
  • 每台设备独立过期;
  • 查询单设备快。

需要注意:

  • Redis key 过期不是严格准时的业务调度器;
  • Keyspace Notification 默认配置和可靠性不适合直接当作唯一离线事件来源;
  • 只看 key 是否存在无法保存完整状态迁移历史;
  • Redis 故障后需要等待设备重新上报或执行重建。

方案二:ZSet 保存离线截止时间

score = last_seen + offline_timeout
member = device_id

心跳到达时更新 score;后台 worker 持续查询截止时间小于当前时间的成员,进行离线状态迁移。

优点:

  • 可以成批找出已超时设备;
  • 离线判定流程更可控;
  • 适合按租户或分片处理。

代价:

  • 需要 worker 和并发控制;
  • 必须避免旧任务把已重新上线的设备误判为离线;
  • 大规模时需要分片,避免一个巨大热点 ZSet。

在线集合与计数

发生确认的状态迁移时:

offline → online:
  SADD online_devices device_id

online → offline:
  SREM online_devices device_id

查询在线数量:

SCARD online_devices

Set 的去重性使重复上线事件相对安全。但它不能自动识别断电设备,仍然依赖超时检测。

如果使用 INCR/DECR 维护纯计数,必须只在真实状态迁移时更新,并定期从状态事实或集合校准。

批量查询一页设备状态

  1. 数据库分页读取 50~100 台设备基础信息;
  2. 提取本页 device_id;
  3. Redis 使用 MGETHMGET 批量读取;
  4. 应用层按 device_id 合并;
  5. 返回这一页,不读取全量十万设备。

这样应用内存只短暂保存一页数据,通常是 KB 到低 MB 级,而不是把全量设备装入内存。

关键故障

重复心跳

心跳只刷新 last_seen,不应每次都生成“上线”事件。只有旧状态不是 online 时才发生状态迁移。

设备突然断电

不会发送离线事件,必须由超时检测产生离线状态。

离线后迅速重连

离线 worker 执行前要再次比较当前版本、last_seen 或 deadline,避免把已重连设备标为离线。

Redis 丢失

  • 设备继续上报后逐步恢复;
  • Broker 会话可以辅助重建;
  • 数据库保留必要的状态迁移历史;
  • 统计值是可重建视图,不把 Redis 当作唯一永久事实。

规模扩大时

十万设备、30 秒心跳的平均到达率约为:

100,000 ÷ 30 ≈ 3,333 条/秒

这只是平均值。真实系统还要考虑同时重连、网络恢复后的突发流量和消息放大。

扩展方向:

  • 状态服务按 tenant_id 或 device_id 分片;
  • Redis Cluster 或多个独立状态分片;
  • 心跳只更新必要字段;
  • 状态迁移事件异步处理;
  • 监控消息积压、处理延迟和状态漂移;
  • 压测突发重连,而不只测均匀流量。

自测

  1. 为什么“收到心跳就 INCR”会统计错误?
  2. TTL key 已过期是否等于离线事件一定已经可靠执行?
  3. Set 为什么不能独立解决突然断电?
  4. 离线 worker 如何避免把刚刚重连的设备标成离线?
  5. Redis 丢失后,哪些数据能够重建,哪些应持久化?

完成标准

  • 能定义“在线”的业务语义
  • 能区分状态事实与查询视图
  • 能比较 TTL 与 ZSet 超时调度
  • 能处理重复心跳、突然断电和快速重连
  • 完成 Redis 在线状态实验

下一单元:大量设备查询