2. Redis 核心数据结构:底层实现与适用场景

Redis 之所以能成为缓存领域的基石,不仅因为其纯内存操作,更在于它提供了五种核心数据结构——String、List、Set、ZSet、Hash。每种结构都对应着不同的底层编码与适用场景。理解这些差异,是设计缓存契约、划定领域边界的前提。

核心认知: Redis 的每种数据结构在内部并非只有一种实现方式。为了平衡内存与性能,Redis 会根据存储数据的大小、元素数量等条件,动态切换底层编码(如 int、embstr、ziplist、skiplist 等)。这种设计哲学贯穿整个课程。

2.1 String:最基础的缓存单元

String 是 Redis 中最简单的数据类型,但也是使用最频繁的。它本质上是一个动态字符串,最大容量为 512 MB。

底层实现

  • int(整数编码): 当存储的字符串可以被解析为 64 位有符号整数时,Redis 直接使用 long 类型存储,节省空间且运算极快。
  • embstr(嵌入式字符串): 当字符串长度小于等于 44 字节时,Redis 使用 embstr 编码。它将 redisObject 和 sdshdr 结构体分配在同一块连续内存中,减少内存碎片,提升缓存命中率。
  • raw(原始字符串): 当字符串长度大于 44 字节时,使用 raw 编码。此时 redisObject 和 sdshdr 分两次分配内存,灵活性更高。

适用场景

场景 说明 示例命令
缓存对象 JSON 将序列化后的对象(如用户信息、配置)直接缓存 SET user:1001 '{"name":"Alice"}'
计数器 利用 INCR/DECR 实现原子自增,如访问次数、库存 INCR page:view:home
分布式锁 通过 SETNX 实现互斥锁,配合过期时间防止死锁 SETNX lock:order 1
Session 共享 在微服务架构中,将用户 Session 以 String 形式集中存储 SETEX session:token 3600 "data"
# 示例:使用 String 实现计数器
> SET article:read:42 0
OK
> INCR article:read:42
(integer) 1
> GET article:read:42
"1"

2.2 List:有序的消息队列

List 是一个双向链表,支持在头部和尾部快速插入、删除元素。它天然适合用作消息队列或时间线存储。

底层实现

  • ziplist(压缩列表): 当列表元素数量较少(默认 < 512 个)且每个元素长度较小(默认 < 64 字节)时,使用 ziplist。它将所有元素连续存储,节省内存。
  • linkedlist(双向链表): 当不满足 ziplist 条件时,转换为 linkedlist。每个节点独立分配内存,支持高效的头部/尾部操作。
  • quicklist(快速列表,Redis 3.2+): 结合了 ziplist 和 linkedlist 的优点。它将链表分段,每段内部使用 ziplist 压缩存储,既节省内存又保持操作效率。

适用场景

场景 说明 示例命令
消息队列 生产者 LPUSH,消费者 RPOP 或 BRPOP(阻塞读取) LPUSH msg:queue "task1"
最新动态/时间线 使用 LTRIM 保留最近 N 条记录,如用户最新发帖 LPUSH user:timeline "post1"
栈结构 利用 LPUSH + LPOP 实现后进先出(LIFO) LPUSH stack "a"
# 示例:使用 List 实现简单消息队列
> LPUSH task:queue "send_email"
(integer) 1
> LPUSH task:queue "generate_report"
(integer) 2
> RPOP task:queue
"send_email"

2.3 Set:去重与集合运算

Set 是一个无序、不重复的字符串集合。它支持高效的集合运算(交集、并集、差集),非常适合标签、关注关系等场景。

底层实现

  • intset(整数集合): 当集合中所有元素都是整数且元素数量较少(默认 < 512 个)时,使用 intset。它使用有序数组存储,支持二分查找,内存紧凑。
  • hashtable(哈希表): 当不满足 intset 条件时,转换为 hashtable。使用字典结构,查找、插入、删除的时间复杂度均为 O(1)。

适用场景

场景 说明 示例命令
标签系统 为文章或商品打标签,通过 SINTER 查找共同标签 SADD article:1:tags "redis" "cache"
去重统计 记录独立访客 IP,利用 Set 自动去重 SADD unique:visitors "192.168.1.1"
关注/粉丝关系 使用 SUNION 或 SDIFF 实现推荐关注 SADD user:1001:follow "user:1002"
# 示例:使用 Set 计算共同好友
> SADD user:1:friends "user:2" "user:3"
(integer) 2
> SADD user:4:friends "user:2" "user:5"
(integer) 2
> SINTER user:1:friends user:4:friends
1) "user:2"

2.4 ZSet:带权重的有序集合

ZSet(Sorted Set)在 Set 的基础上为每个元素关联了一个 score(分数),元素按 score 从小到大排序。它是实现排行榜、延迟队列等场景的核心数据结构。

底层实现

  • ziplist(压缩列表): 当元素数量较少(默认 < 128 个)且每个元素长度较小时,使用 ziplist。元素按 score 顺序存储,查找时使用二分法。
  • skiplist + hashtable(跳表 + 哈希表): 当不满足 ziplist 条件时,使用 skiplist 作为有序结构,hashtable 作为索引结构。跳表支持 O(log N) 的插入、删除和范围查询,哈希表则保证元素到 score 的快速映射。

适用场景

场景 说明 示例命令
排行榜 按玩家分数排序,使用 ZRANK 获取排名 ZADD leaderboard 100 "player1"
延迟队列 将任务执行时间作为 score,使用 ZRANGEBYSCORE 获取到期任务 ZADD delay:queue 1700000000 "task1"
滑动窗口限流 将时间戳作为 score,统计窗口内的请求数 ZADD rate:limit:api 1700000000 "req1"
# 示例:使用 ZSet 实现排行榜
> ZADD game:score 100 "Alice"
(integer) 1
> ZADD game:score 200 "Bob"
(integer) 1
> ZADD game:score 150 "Charlie"
(integer) 1
> ZRANGE game:score 0 -1 WITHSCORES
1) "Alice"
2) "100"
3) "Charlie"
4) "150"
5) "Bob"
6) "200"

2.5 Hash:对象字段的紧凑存储

Hash 是一个键值对集合,特别适合存储对象。与将整个对象序列化为 String 不同,Hash 允许对单个字段进行读写,节省带宽并支持部分更新。

底层实现

  • ziplist(压缩列表): 当字段数量较少(默认 < 512 个)且每个字段和值的长度较小时,使用 ziplist。它将所有键值对按顺序存储,内存利用率极高。
  • hashtable(哈希表): 当不满足 ziplist 条件时,转换为 hashtable。使用字典结构,支持 O(1) 的字段读写。

适用场景

场景 说明 示例命令
用户信息缓存 存储用户对象的多个字段,如姓名、年龄、邮箱 HSET user:1001 name "Alice" age 30
购物车 以用户 ID 为 key,商品 ID 为 field,数量为 value HSET cart:1001 item:2001 2
配置管理 存储应用配置,支持按字段单独更新 HSET app:config theme "dark"
# 示例:使用 Hash 存储用户信息
> HSET user:1001 name "Alice" age 30 email "alice@example.com"
(integer) 3
> HGET user:1001 name
"Alice"
> HGETALL user:1001
1) "name"
2) "Alice"
3) "age"
4) "30"
5) "email"
6) "alice@example.com"

2.6 数据结构选择决策树

在实际缓存契约设计中,选择哪种数据结构往往取决于访问模式和数据关系。以下决策树可以帮助你快速判断:

Redis 数据结构选择决策树 数据是什么类型? 简单值 / 计数器 → String 复杂结构 需要排序? 是 → ZSet 否 → 去重? 是 → Set 否 → 对象字段? 是 → Hash 否 → 考虑 List 提示 List 适合消息队列 或时间线场景

2.7 底层编码对比总结

下表汇总了五种数据结构的底层编码及其切换条件,帮助你在设计缓存时预判内存与性能表现:

数据结构 底层编码 切换条件 时间复杂度 内存效率
String int / embstr / raw 长度 ≤ 44 字节 → embstr;否则 raw O(1) 极高(int 编码)
List ziplist / linkedlist / quicklist 元素 < 512 且长度 < 64B → ziplist O(1) 头尾操作 高(ziplist)
Set intset / hashtable 元素 < 512 且全为整数 → intset O(1) 高(intset)
ZSet ziplist / skiplist+hashtable 元素 < 128 且长度 < 64B → ziplist O(log N) 中(skiplist)
Hash ziplist / hashtable 字段 < 512 且长度 < 64B → ziplist O(1) 高(ziplist)
避坑指南: 不要盲目使用 ZSet 替代 List 或 Set。虽然 ZSet 功能强大,但其底层跳表结构在插入和删除时涉及多层指针调整,性能开销远大于 List 的头尾操作。只有在确实需要按 score 排序或范围查询时,才选择 ZSet。

2.8 实战:缓存契约中的数据结构选择

假设我们正在设计一个文章缓存系统,需要缓存文章详情、阅读量、标签、排行榜和评论列表。根据上述知识,我们可以做出如下契约设计:

缓存需求 推荐数据结构 Key 设计 理由
文章详情 Hash article:{id} 支持按字段更新(如标题、内容),节省带宽
阅读量 String article:read:{id} 原子自增,高性能计数器
文章标签 Set article:tags:{id} 自动去重,支持交集运算(推荐相关文章)
热门文章排行榜 ZSet hot:articles 按阅读量排序,支持范围查询
最新评论 List article:comments:{id} LPUSH 新评论,LTRIM 保留最近 N 条

通过这种契约设计,我们不仅明确了每个缓存键的职责边界,还充分利用了 Redis 各数据结构的底层优势,实现了内存效率与访问性能的最佳平衡。