2. Redis 核心数据结构及其适用场景
Redis 之所以能成为缓存与实时数据领域的首选,核心在于其丰富的数据结构。与仅支持字符串的 Memcached 不同,Redis 提供了五种基本数据类型,每种都针对特定操作进行了优化。理解这些结构的底层实现与适用场景,是设计高效缓存契约的前提。
2.1 数据结构全景概览
下表从底层编码、时间复杂度、最大容量三个维度,对比五种核心数据结构:
| 数据结构 | 底层编码(3.2+) | 关键操作时间复杂度 | 单 Key 最大容量 |
|---|---|---|---|
| String | int / embstr / raw | GET/SET: O(1) | 512 MB |
| Hash | ziplist / hashtable | HGET/HSET: O(1) | 2^32 - 1 个 field |
| List | quicklist | LPUSH/RPOP: O(1) LINDEX: O(N) |
2^32 - 1 个元素 |
| Set | intset / hashtable | SADD/SISMEMBER: O(1) | 2^32 - 1 个元素 |
| Sorted Set | ziplist / skiplist + dict | ZADD: O(log N) ZRANK: O(log N) |
2^32 - 1 个元素 |
2.2 String — 最通用的缓存载体
String 是 Redis 最基础的数据类型,可以存储文本、数字、二进制数据(如图片序列化后的字节流)。其内部编码会根据值长度自动切换:
- int:值可被解析为 64 位有符号整数时使用,支持 INCR/DECR 原子操作。
- embstr:值长度 ≤ 44 字节时使用,内存分配与释放只需一次。
- raw:值长度 > 44 字节时使用,需要两次内存分配。
典型适用场景:
- 会话 Token / Session 缓存
- 页面片段缓存(HTML 片段、JSON 响应体)
- 计数器(访问次数、库存扣减)
- 分布式锁(SETNX + 过期时间)
# 缓存用户会话信息
SET session:user:1001 "{\"name\":\"Alice\",\"role\":\"admin\"}" EX 3600
# 原子计数器
INCR article:readcount:42
# 分布式锁
SET lock:order:pay "uuid-xxx" NX EX 10
2.3 Hash — 对象结构化存储
Hash 适合存储对象类型的数据,每个 Hash 可以包含多个 field-value 对。相比将整个对象序列化为 String,Hash 允许对单个字段进行读写,节省带宽并支持部分更新。
典型适用场景:
- 用户资料缓存(姓名、邮箱、头像 URL 等字段)
- 购物车(商品 ID 为 field,数量为 value)
- 配置项集合(应用级动态配置)
# 存储用户资料
HSET user:1001 name "Alice" email "alice@example.com" age 28
# 只获取邮箱字段
HGET user:1001 email
# 自增年龄字段
HINCRBY user:1001 age 1
2.4 List — 有序队列与消息缓冲
List 是基于双向链表实现的有序集合,支持在头部和尾部快速插入/删除。Redis 3.2 之后使用 quicklist 编码,结合了 ziplist 和 linkedlist 的优点。
典型适用场景:
- 消息队列(LPUSH + BRPOP 实现生产者-消费者模式)
- 最新动态列表(LPUSH + LTRIM 保留最近 N 条)
- 时间线(Timeline)数据
# 生产者:推送消息到队列
LPUSH task:queue "task:123" "task:124"
# 消费者:阻塞式获取消息
BRPOP task:queue 0
# 保留最近 100 条用户动态
LPUSH user:timeline:1001 "post:1001"
LTRIM user:timeline:1001 0 99
2.5 Set — 唯一性与集合运算
Set 是无序的字符串集合,自动去重。底层使用 hashtable(元素较多时)或 intset(元素为整数且数量较少时)。Set 最强大的特性是支持交集、并集、差集运算,这些运算在服务端完成,避免了客户端数据传输。
典型适用场景:
- 标签系统(文章标签、用户兴趣标签)
- 好友关系(共同好友查询)
- 去重统计(独立访客 UV)
- 抽奖池(SADD 添加参与者,SRANDMEMBER 随机抽取)
# 为文章添加标签
SADD article:42:tags "redis" "cache" "database"
# 查询两篇文章的共同标签
SINTER article:42:tags article:100:tags
# 随机抽取 3 名中奖用户
SRANDMEMBER lottery:pool 3
2.6 Sorted Set — 带权重的有序集合
Sorted Set 在 Set 的基础上为每个元素关联了一个 double 类型的 score(分数),元素按 score 排序。其底层使用 skiplist + dict 的组合结构,兼顾了高效的插入/删除和范围查询。
典型适用场景:
- 实时排行榜(游戏分数、商品销量)
- 延迟队列(score 存储执行时间戳)
- 滑动窗口限流(score 存储时间戳,ZREMRANGEBYSCORE 清理过期数据)
- 自动补全(score 存储权重)
# 游戏排行榜:玩家分数
ZADD leaderboard:game1 9527 "player:1001"
ZADD leaderboard:game1 8800 "player:1002"
ZADD leaderboard:game1 10200 "player:1003"
# 获取前三名(带分数)
ZREVRANGE leaderboard:game1 0 2 WITHSCORES
# 获取玩家排名
ZRANK leaderboard:game1 "player:1001"
2.7 数据结构选择决策流程
下面的 SVG 图展示了在缓存契约设计中选择数据结构的决策路径:
2.8 数据结构与缓存契约的映射关系
在缓存契约设计中,每种数据结构对应不同的 API 语义。下面的 SVG 图展示了数据结构与契约模式的对应关系:
2.9 实战选型清单
在缓存契约设计阶段,建议按以下清单快速确定数据结构:
- 数据是标量还是对象? 标量用 String,对象用 Hash。
- 是否需要排序? 是 → Sorted Set;否 → 继续判断。
- 是否需要去重? 是 → Set;否 → 继续判断。
- 是否需要两端操作? 是 → List;否 → 考虑 String 或 Hash。
- 是否需要集合运算? 是 → Set 或 Sorted Set(带权重交集)。
- 是否需要过期时间? 所有结构都支持 EXPIRE,但 String 最常用。
"type": "sorted_set",并附带 "score_field": "score" 说明。这能让客户端 SDK 自动选择正确的 Redis 命令进行数据操作,避免运行时类型错误。