2. Redis核心数据结构选型
Redis 之所以能同时承担缓存、计数器、排行榜、消息队列等多种角色,根本原因在于它提供了六种语义完全不同的核心数据结构。选型错误往往不会立刻报错,却会在数据量增长后表现为内存暴涨、命令复杂度升高、甚至阻塞主线程。因此,理解每种结构的“数据形状”和“内存代价”,是缓存平台设计的第一道关口。
一、六种结构的语义定位
String 是最基础的键值类型,值可以是文本、JSON、序列化对象或二进制,单键最大 512MB。它适合整存整取的场景,例如用户会话、页面片段、配置项、计数器(配合 INCR)。Hash 是字段-值映射,适合存储对象的多个属性,可以只读写其中某个字段,避免整体序列化开销。List 是有序、可重复的双向链表(底层为 quicklist),支持两端推入弹出,天然适合最新列表、简单队列。Set 是无序、去重的集合,支持交并差运算,适合标签、去重、共同好友。ZSet 在 Set 基础上为每个成员绑定一个 score,按 score 排序,是排行榜、延时队列的核心。Stream 是 Redis 5.0 引入的追加日志结构,带消息 ID、消费者组和 ACK 机制,用于可靠消息流。
二、内存开销的关键影响因素
Redis 的内存开销并不只等于“数据本身大小”,还包括键对象、值对象、过期字典、编码转换后的额外结构。以 String 为例,一个短字符串会使用 embstr 编码,把 RedisObject 和 SDS 分配在同一块内存中,减少一次内存分配;当长度超过 44 字节后转为 raw 编码,需要两次分配。这意味着同样存 100 万个短值,embstr 与 raw 的内存差距可能达到 20% 以上。
Hash 的内存表现高度依赖编码。当字段数不超过 128 且每个字段值不超过 64 字节时,Hash 使用 listpack(旧版本为 ziplist)编码,数据紧凑、无指针开销;一旦超过阈值,转为 hashtable,每个字段都要维护哈希桶和指针,内存可能翻倍。因此,把一个大对象拆成 Hash 存储时,要控制字段数量,避免无意中触发编码转换。
ZSet 是内存开销较高的结构,因为它同时维护一个 dict(成员到 score 的映射)和一个 skiplist(按 score 排序)。每个成员至少被存储两次,再加上跳表的多层指针,内存通常是同规模 Set 的 1.5~2 倍。如果只需要去重而不需要排序,应优先用 Set;如果只需要排序而不需要成员去重,可以考虑用 List 加外部排序。
Stream 的内存与消息保留策略直接相关。默认情况下消息会一直保留,必须显式使用 XADD 的 MAXLEN 或 MINID 参数进行裁剪。例如 XADD mystream MAXLEN ~ 10000 * field value 表示大致保留最近 1 万条,~ 允许 Redis 在节点边界处裁剪,效率更高。如果不加限制,Stream 会持续增长,最终触发 maxmemory 淘汰或写入失败。
三、业务场景选型对照
| 业务场景 | 推荐结构 | 关键理由 | 需规避的做法 |
|---|---|---|---|
| 用户会话 token | String | 整存整取,可设 TTL | 拆成多个键导致键数量膨胀 |
| 商品属性缓存 | Hash | 可单独更新价格、库存 | 字段数超过 128 触发 hashtable |
| 最新 100 条评论 | List | LPUSH + LTRIM 天然截断 | 用 LRANGE 全量读取大 List |
| 文章标签去重 | Set | SADD 自动去重,支持交集 | 元素为非整数且量大时内存高 |
| 销量排行榜 | ZSet | ZINCRBY + ZREVRANGE 高效 | 用 List 手动排序,复杂度高 |
| 订单事件流 | Stream | 消费者组 + ACK 保证可靠 | 不设 MAXLEN 导致无限增长 |
四、一个可执行的选型判断过程
假设要为一个内容社区设计缓存,需求是:缓存文章详情、维护文章点赞用户集合、生成热度排行榜、记录用户浏览历史、投递站内通知。可以按以下顺序判断:
- 文章详情:字段固定且需要单独更新阅读数,选择 Hash,键为
article:{id},字段包括 title、author、content、read_count。若 content 很大,可单独用 String 存article:content:{id},避免 Hash 整体膨胀。 - 点赞用户集合:需要去重和判断某用户是否点赞,选择 Set,键为
article:likes:{id},用 SADD 添加、SISMEMBER 判断、SCARD 计数。 - 热度排行榜:需要按热度排序并取 Top N,选择 ZSet,键为
rank:hot,用 ZINCRBY 更新分数,ZREVRANGE 取前 100。 - 用户浏览历史:需要保留最近 N 条且有序,选择 List,键为
history:{userId},LPUSH 写入后 LTRIM 保留 50 条。 - 站内通知:需要可靠投递、支持多消费者,选择 Stream,键为
notify:stream,用 XADD 写入并设置 MAXLEN,消费者组用 XREADGROUP 读取、XACK 确认。
最后需要强调,内存开销对比不是绝对的数值排名,而是随元素数量、值长度、编码方式动态变化的。实际部署中应通过 MEMORY USAGE key 观察单个键的真实占用,用 INFO memory 跟踪整体使用量,再结合业务增长趋势决定是否拆分键、调整编码阈值或引入淘汰策略。选型的终点不是记住结论,而是建立“数据形状 → 结构语义 → 内存代价 → 业务约束”的推理链条。