2. 会话与实时状态的数据建模

在 Redis 会话与实时状态平台中,数据建模决定了系统能否在毫秒级完成状态读写、能否在用户量增长时保持内存可控、能否在故障恢复后快速重建状态。本节围绕用户会话、在线状态、设备状态、业务状态四类核心对象,讨论如何选择 Redis 数据结构、如何规划键空间,以及如何用 TTL、Hash、Set、Sorted Set、Bitmap、Stream 等能力表达真实业务语义。

一、四类状态对象的语义差异

用户会话(Session)通常是一次登录后产生的短期凭证与上下文,生命周期以分钟到小时计,要求读写快、可续期、可主动失效。在线状态(Presence)表达“某用户当前是否在线、在哪个节点、最后心跳时间”,特点是高频更新、需要按用户或按房间聚合查询。设备状态(Device State)描述终端连接信息,如设备 ID、平台、推送 Token、连接网关、最后活跃时间,往往一个用户对应多台设备。业务状态(Business State)则是与具体业务绑定的实时数据,例如购物车草稿、未读计数、房间成员、任务进度、限流令牌等,生命周期和一致性要求差异很大。

这四类对象不能共用同一种结构。会话适合 Hash 加 TTL;在线状态适合 Set 或 Sorted Set 加心跳;设备状态适合 Hash 或 String 加索引;业务状态则要根据读写模式选择 String、Hash、Set、Sorted Set、Bitmap 或 Stream。建模的第一步不是选结构,而是先明确访问模式:谁写、谁读、读多少、写多频繁、是否需要范围查询、是否要求原子性。

二、键空间规划:命名、分片与生命周期

键空间规划的核心是让键名可读、可扫描、可治理。推荐采用“业务域:对象类型:标识:维度”的分段命名,例如 sess:user:10086、presence:room:2001、device:user:10086、biz:cart:10086。冒号分段便于用 SCAN 按前缀统计,也便于在监控中按业务域聚合内存。不要使用无意义的自增 ID 作为唯一键名,否则排障时无法快速定位归属。

TTL 是会话与在线状态的生命线。会话键应设置滑动过期,例如 30 分钟,每次请求续期;在线状态应设置心跳过期,例如 60 秒,由客户端每 20 秒上报一次。设备状态可以设置较长 TTL,例如 7 天,避免离线设备长期占用内存。业务状态则按业务语义决定:购物车可设 7 天,未读计数可长期保留,限流令牌必须精确到秒级过期。

分片方面,单实例键数量建议控制在千万级以内,单键 Value 不宜超过 10 KB。对于大 Hash 或大 Set,应按用户 ID 或房间 ID 做哈希分片,例如 presence:room:{roomId} 使用 Hash Tag 保证同一房间落在同一槽位,便于 Lua 脚本原子操作。键空间规划还要预留“冷热分离”:热数据放内存,冷数据可落盘或转存到其他存储,Redis 只保留索引和短期状态。

三、数据结构选型与典型操作

会话数据推荐使用 Hash,字段包括 uid、loginAt、ip、ua、roles、csrf 等。Hash 可以只更新单个字段,避免整体序列化开销。示例:

HSET sess:user:10086 uid 10086 loginAt 1710000000 ip 10.0.0.8 ua "iOS/17.4"
EXPIRE sess:user:10086 1800
HGET sess:user:10086 uid

在线状态推荐使用 Set 或 Sorted Set。若只需判断“是否在线”,用 Set 存用户 ID,配合 SISMEMBER 判断;若需要按最后心跳时间排序、清理超时用户,用 Sorted Set,score 存心跳时间戳,ZRANGEBYSCORE 可批量找出超时成员。房间在线列表可用 SADD presence:room:2001 10086,离线时 SREM。若在线人数规模很大,可用 Bitmap 按用户 ID 偏移量置位,内存效率极高,但要求用户 ID 连续且范围可控。

设备状态推荐使用 Hash 存储单设备详情,再用 Set 维护用户到设备的索引,例如 SADD device:user:10086 devA devB。查询某用户所有设备时先取 Set,再批量 HMGET 各设备 Hash。业务状态中,未读计数用 INCR 或 HINCRBY;排行榜用 Sorted Set;消息流用 Stream,配合 XADD 和 XREADGROUP 实现消费组;限流用 String 加 INCR 和 EXPIRE,或 Lua 脚本保证原子性。

会话与实时状态数据模型:对象 → 结构 → 键空间 用户会话 Session 登录凭证 / 上下文 TTL 30min 滑动续期 在线状态 Presence 心跳 / 房间成员 TTL 60s 高频更新 设备状态 Device 设备 ID / 平台 / Token TTL 7d 多端索引 业务状态 Business 计数 / 排行 / 消息流 按业务语义定 TTL Hash 字段级更新 + EXPIRE Set / Sorted Set / Bitmap 成员判断 / 超时清理 / 位图 Hash + Set 索引 用户 → 设备列表 → 详情 String / ZSet / Stream 计数 / 排行 / 消费组 键空间命名规范:业务域:对象类型:标识:维度 sess:user:10086 | presence:room:2001 | device:user:10086 | biz:cart:10086 | biz:rank:2024-06 Hash Tag 保证同房间同槽位;SCAN 按前缀统计;TTL 分层:会话 30min / 在线 60s / 设备 7d / 业务按需 会话写入与续期 HSET sess:user:10086 uid 10086 ip 10.0.0.8 EXPIRE sess:user:10086 1800 在线心跳与超时清理 ZADD presence:room:2001 1710000000 10086 ZRANGEBYSCORE presence:room:2001 -inf 1709999940

四、一致性、并发与故障恢复

Redis 单命令原子,但跨键操作需要 Lua 脚本或事务。例如“用户登录时写入会话并加入在线集合”,可用 Lua 将 HSET、EXPIRE、SADD 组合,避免中间状态被其他请求观察到。对于在线状态,心跳更新与超时清理存在竞争:清理任务可能误删刚续期的用户。解决办法是用 Sorted Set 的 score 作为心跳时间,清理时只删除 score 小于阈值的成员,并在删除前用 Lua 再次校验。

故障恢复方面,会话数据若允许丢失,可只依赖 TTL 和客户端重登;若要求强一致,应开启 AOF everysec 并配合主从复制。在线状态属于可重建数据,节点重启后可由客户端心跳重新填充,因此不必持久化。设备状态和业务状态则要根据业务容忍度决定是否落盘。键空间还应设置内存告警,例如 maxmemory-policy allkeys-lru 适合缓存型会话,volatile-lru 适合只淘汰带 TTL 的键,避免误删长期业务数据。

五、建模检查清单

  • 每个键是否有明确业务域前缀和 TTL 策略?
  • 高频更新字段是否用 Hash 或 Sorted Set 避免整体重写?
  • 是否存在大 Key 风险?单 Hash 字段数是否超过 1000?单 Set 成员是否超过 5000?
  • 跨键操作是否用 Lua 保证原子性?
  • 在线状态是否可重建?会话是否可主动失效?
  • 是否按房间或用户做了 Hash Tag 分片,避免单槽热点?

建模不是一次性的设计,而是随着业务读写模式演进的持续过程。建议在平台中保留键空间字典和采样监控,定期用 SCAN 加 MEMORY USAGE 检查大 Key 与过期分布,让 Redis 中的每一类状态都对应清晰的生命周期和访问路径。