缓存策略
Redis 在平台中的使用规范,包括缓存分层、Key 设计、TTL 策略和设备影子存储方案。
1. 缓存分层
Redis 在平台中承担以下角色:
| 分层 | 存储内容 | 数据结构 | 说明 |
|---|---|---|---|
| 在线状态 | 设备上下线标记 | String | device:online:{id} = 1/0 |
| 设备影子(热缓存) | 设备最新一次上报的属性值 | Hash | 仅供实时读取和 WebSocket 推送,不替代 PostgreSQL 中的影子权威数据 |
| 指令状态 | 待响应指令的临时追踪 | String + TTL | 下发指令后记录 msgId,收到 set_reply 后清除 |
| 用户会话 | JWT 黑名单 / 刷新令牌 | String | 登出或 Token 刷新时写入 |
| 限流计数 | API 请求频率计数器 | String + TTL | 配合 ThrottlerGuard 使用 |
2. Key 命名规范
统一命名格式:{domain}:{entity}:{id}:{field}
| Key 示例 | 说明 |
|---|---|
device:online:d1a2b3c4 | 设备 d1a2b3c4 的在线状态 |
device:shadow:d1a2b3c4 | 设备 d1a2b3c4 的影子缓存(Hash,field=属性名,value=属性值) |
cmd:status:msg12345 | 指令 msg12345 的执行状态 |
user:token:1001 | 用户 1001 的刷新令牌 |
rate:user:1001:/api/v1/devices | 用户 1001 对某接口的请求计数 |
命名原则:
- 全小写,冒号分隔层级
- 不包含空格和特殊字符
- 总长度不超过 128 字符
- 语义从大到小排列(域 → 实体 → ID → 字段)
3. 设备影子:Redis vs PostgreSQL
这是本文档最重要的部分。当前文档中存在矛盾:04.数据模型概览 说影子放 PostgreSQL,而 01.系统架构设计 的时序图中把影子写入了 Redis。
结论:两级存储,各司其职。
存储分工
| 存储 | 角色 | 数据内容 | 读写场景 |
|---|---|---|---|
| Redis | 热缓存(非权威) | 设备最近一次上报的属性值(Hash,field=属性名,value=属性值) | 实时推送(WebSocket 读)、MQTT 消息更新(写) |
| PostgreSQL | 权威数据源 | 完整的影子 JSON(desired + reported + version + updatedAt) | API 查询历史影子、审计追溯(读)、控制指令后的期望状态更新(写) |
同步策略
1. 设备上报属性
MQTT 消息到达 → 后端处理
├── Redis: HSET device:shadow:{id} temp 45.2 relay_1 1(覆盖写,低延迟)
└── PostgreSQL: UPDATE device_shadow SET reported = merge(reported, newData), updated_at = now()同时写入两边,Redis 先写(保证推送实时性),PostgreSQL 后写(保证数据持久化)。
2. 用户查询设备详情(API)
直接读 PostgreSQL 影子 → 返回完整 JSON(含 desired + reported + version)。不依赖 Redis,因为 Redis 可能刚过期或重启丢失。
3. WebSocket 推送
读 Redis Hash → 构造推送消息。高频、低延迟场景。Redis 不可用时降级读取 PostgreSQL(延迟略高但数据不丢失)。
4. 云端修改期望状态(控制指令)
写 PostgreSQL 影子(desired 部分)→ 通过 MQTT 下发指令 → 设备执行后上报 actual 状态 → 回到步骤 1 的同步流程。
5. Redis 故障/重启
- 影子缓存首次访问时从 PostgreSQL 回填:
HGETALL不存在 →SELECT reported FROM device_shadow WHERE device_id = ?→HSET回填 - 无数据丢失风险,PostgreSQL 始终持有完整影子
- 回填期间 WebSocket 降级读 PostgreSQL(增加约 5ms 延迟,可接受)
6. TTL 设置
- 影子缓存 TTL:30 分钟
- 设备持续上报属性时自动续期(每次 HSET 后 EXPIRE 刷新)
- 无上报则 30 分钟后自动过期,释放内存
- 下次访问时从 PostgreSQL 重新回填
同步到其他文档
以下文档中的矛盾需要在后续 PR 中修正:
| 文档 | 需要修改的内容 |
|---|---|
04.数据模型概览.md | "存储分层"表中"设备影子"行:存储列从"PostgreSQL (JSON)"改为"PostgreSQL (JSON) + Redis (Hash 热缓存)";说明列补充"两级存储:PostgreSQL 为权威源,Redis 为实时读写缓存" |
01.系统架构设计.md | "数据上报"时序图相关的详细步骤:"更新 Redis 中的设备影子"改为"更新 Redis 影子缓存 + 写入 PostgreSQL 影子" |
4. TTL 策略汇总
| 缓存类型 | TTL | 续期 | 过期后行为 |
|---|---|---|---|
| 在线状态 | 120 秒 | 设备心跳或新消息到达时续期 | 判定设备离线,Redis key 自动过期 |
| 设备影子 | 30 分钟 | 每次属性上报续期 | 自动清理,下次访问从 PostgreSQL 回填 |
| 指令状态 | 30 秒 | 不续期 | 判定指令超时,标记为失败并通知前端 |
| 用户会话 | 7 天 | 刷新 Token 时续期 | 用户需重新登录 |
| 限流计数 | 60 秒 | 不续期 | 计数器归零,下个窗口重新计数 |
5. 缓存穿透/击穿/雪崩防护
| 问题 | 场景 | 应对 |
|---|---|---|
| 缓存穿透 | 查询不存在的设备 ID,绕过缓存直接打到 PostgreSQL | 对空结果也缓存短 TTL(60 秒),值标记为 NULL,下次命中直接返回 |
| 缓存击穿 | 热点设备影子过期瞬间,大量请求同时打到 PostgreSQL | 分布式锁(Redis SETNX),只有一个请求去查 DB 并回填缓存,其他请求等待锁释放后读缓存 |
| 缓存雪崩 | 大量缓存在同一时刻过期 | TTL 加随机偏移(±20%),避免集中失效;核心数据(在线状态)设置永不过期 + 手动清理 |
6. 与 EMQX 的 Redis 使用边界
EMQX 自身也会使用 Redis 作为内部缓存(如会话持久化、共享订阅状态),与后端业务 Redis 需区分:
| 方案 | 说明 |
|---|---|
| 不同 DB 编号 | EMQX 使用 DB 0,后端使用 DB 1(配置简单,推荐) |
| 不同 Key 前缀 | EMQX 用 emqx: 前缀,后端用 device: / user: / cmd: 等前缀(同一 DB 内互不冲突) |
EMQX 侧的 Redis 配置见 09.EMQX配置与安全。
