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 个元素
契约设计提示: 在 API 契约中声明数据结构类型,能帮助客户端预判操作复杂度。例如,List 的 LINDEX 是 O(N) 操作,不适合随机访问场景;而 Sorted Set 的 ZRANGEBYSCORE 是 O(log N + M),适合排行榜分页。

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
契约设计建议: 当对象字段数量固定且较少(通常 < 64 个)时,Hash 使用 ziplist 编码,内存效率极高。在 API 契约中,建议将 Hash 的 field 集合定义为枚举或固定列表,避免客户端传入未知字段导致内存膨胀。

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
注意: List 的 LINDEX 和 LSET 操作需要遍历链表,时间复杂度为 O(N)。如果需要频繁随机访问,应改用 Sorted Set 或 Hash 结构。在缓存契约中,应明确 List 的访问模式是“两端操作”还是“随机访问”。

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"
性能提示: ZRANGEBYSCORE 和 ZREVRANGEBYSCORE 支持分页查询,适合排行榜前端展示。当 Sorted Set 元素数量超过 zset-max-ziplist-entries(默认 128)或元素长度超过 zset-max-ziplist-value(默认 64)时,编码会从 ziplist 切换为 skiplist,写入性能略有下降但查询性能保持稳定。

2.7 数据结构选择决策流程

下面的 SVG 图展示了在缓存契约设计中选择数据结构的决策路径:

Redis 数据结构选择决策树 数据需要什么能力? 简单键值 / 计数器 复杂结构 / 集合运算 String 需要排序 / 权重? 需要唯一性? Sorted Set List(队列) Set Hash(对象) 决策要点: 1. 需要原子计数、简单缓存 → String 2. 需要按分数排序、范围查询 → Sorted Set 3. 需要去重、交集/并集运算 → Set 4. 需要对象字段独立操作 → Hash;需要消息队列 → List

2.8 数据结构与缓存契约的映射关系

在缓存契约设计中,每种数据结构对应不同的 API 语义。下面的 SVG 图展示了数据结构与契约模式的对应关系:

数据结构 → 缓存契约模式映射 数据结构 String Hash List Set Sorted Set 缓存契约模式 / API 语义 GET /cache/{key} → 直接返回;支持原子 INCR/DECR GET /cache/{key}/{field} → 部分读取;PATCH 支持部分更新 POST /queue/{name} → 入队;GET /queue/{name} → 出队(阻塞/非阻塞) POST /set/{key} → 添加元素;GET /set/{key}/intersect → 交集运算 GET /leaderboard/{name}?range=0:9 → 分页排行;支持权重更新

2.9 实战选型清单

在缓存契约设计阶段,建议按以下清单快速确定数据结构:

  1. 数据是标量还是对象? 标量用 String,对象用 Hash。
  2. 是否需要排序? 是 → Sorted Set;否 → 继续判断。
  3. 是否需要去重? 是 → Set;否 → 继续判断。
  4. 是否需要两端操作? 是 → List;否 → 考虑 String 或 Hash。
  5. 是否需要集合运算? 是 → Set 或 Sorted Set(带权重交集)。
  6. 是否需要过期时间? 所有结构都支持 EXPIRE,但 String 最常用。
契约设计最佳实践: 在 API 契约的响应体中,使用明确的字段名标识数据结构类型。例如:"type": "sorted_set",并附带 "score_field": "score" 说明。这能让客户端 SDK 自动选择正确的 Redis 命令进行数据操作,避免运行时类型错误。