2. Redis 核心数据结构:String、Hash、List、Set、Sorted Set 的底层编码与适用场景

Redis 之所以能成为缓存与实时数据处理的标杆,核心在于其五种基础数据结构的灵活性与底层编码优化。理解每种结构的底层实现(编码)与适用场景,是设计高命中率缓存策略、优化调度平台索引选择的前提。本章将逐一拆解 String、Hash、List、Set、Sorted Set 的内部编码机制与最佳实践。

2.1 数据结构总览与编码决策

Redis 会根据存储数据的大小、元素数量、元素类型,动态选择最节省内存的底层编码。这种“多态”设计使得同一数据结构在不同负载下表现迥异。下表总结了五种结构的默认编码与切换条件:

数据结构 底层编码(Redis 7+) 编码切换条件 核心优势
String int / embstr / raw 整数用 int;≤44 字节用 embstr;>44 字节用 raw 简单、原子操作、数值计算
Hash ziplist / listpack / hashtable 元素少且值小时用 listpack;否则用 hashtable 对象字段存储、内存紧凑
List quicklist(ziplist 节点) 固定使用 quicklist,节点内压缩 双向操作、消息队列
Set intset / hashtable 全整数且元素少时用 intset;否则用 hashtable 去重、交并差运算
Sorted Set ziplist / listpack / skiplist + dict 元素少且值小时用 listpack;否则用 skiplist + dict 排序、范围查询、排行榜
编码选择原则: 小数据用紧凑编码(listpack/intset),大数据用高效哈希/跳表。调度平台索引选择时,应优先预估数据规模,避免因编码切换导致的内存抖动。

2.2 String:最基础的原子载体

String 是 Redis 最基础的结构,可存储文本、整数、浮点数、二进制数据(如图片序列化)。其底层编码分为三种:

  • int:存储 64 位有符号整数,直接以 long 类型存储,无额外内存开销。
  • embstr:存储 ≤44 字节的字符串,Redis 对象和 SDS(Simple Dynamic String)分配在同一块连续内存,减少内存碎片。
  • raw:存储 >44 字节的字符串,对象和 SDS 分开分配,支持动态扩容。

适用场景:

  • 缓存热点数据(如用户 Session、配置项)。
  • 计数器(incr/decr 原子操作)。
  • 分布式锁(SETNX + 过期时间)。
  • 位图统计(SETBIT/GETBIT)。
# 示例:缓存用户登录次数
SET user:login:1001 0
INCR user:login:1001
GET user:login:1001

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

Hash 适合存储结构化对象(如用户信息、商品详情),避免 String 的 key 膨胀问题。底层编码在元素少(默认 <512 个)且每个字段值小(默认 <64 字节)时使用 listpack(Redis 7+ 替代 ziplist),否则切换为 hashtable。

  • listpack:连续内存块,每个 entry 包含编码、长度、数据,无指针开销,内存利用率极高。
  • hashtable:使用 dict 结构,支持 O(1) 读写,但每个键值对需额外存储指针和哈希表头。

适用场景:

  • 存储对象属性(如用户姓名、年龄、邮箱)。
  • 缓存数据库行记录,减少序列化开销。
  • 调度平台中存储任务元数据(任务 ID、状态、优先级)。
# 示例:存储用户信息
HSET user:1001 name "Alice" age 30 city "Beijing"
HGETALL user:1001
性能提示: 当 Hash 字段数超过 1000 或单个值超过 1KB 时,listpack 的更新性能会下降,建议评估是否切换为 hashtable 或拆分结构。

2.4 List:双向链表与消息队列

List 底层使用 quicklist,它是一个由多个 ziplist(或 listpack)节点组成的双向链表。每个节点内部是紧凑的压缩列表,节点之间通过指针连接。这种设计兼顾了内存紧凑与两端操作的高效。

  • LPUSH / RPUSH:O(1) 在头/尾插入。
  • LPOP / RPOP:O(1) 移除头/尾元素。
  • LINDEX / LSET:O(n) 随机访问,n 为索引偏移。

适用场景:

  • 消息队列(生产者 LPUSH,消费者 RPOP/BRPOP)。
  • 最新动态列表(如用户时间线,LPUSH + LTRIM 限制长度)。
  • 栈(LPUSH + LPOP)或队列(LPUSH + RPOP)。
# 示例:实现简单任务队列
LPUSH task:queue "task:001"
RPOP task:queue

2.5 Set:无序去重与集合运算

Set 底层编码在元素全为整数且数量较少(默认 <512 个)时使用 intset,否则使用 hashtable。

  • intset:有序整数数组,支持二分查找,内存紧凑。
  • hashtable:使用 dict 存储,value 为 NULL,支持 O(1) 增删查。

适用场景:

  • 标签系统(用户兴趣标签、文章分类)。
  • 去重(如 IP 白名单)。
  • 集合运算(SINTER 共同好友、SUNION 合并权限)。
# 示例:计算两个用户的共同兴趣
SADD user:1001:tags "sports" "music" "tech"
SADD user:1002:tags "music" "art" "tech"
SINTER user:1001:tags user:1002:tags

2.6 Sorted Set:排序与范围查询的利器

Sorted Set 是 Redis 最强大的结构之一,每个元素关联一个 double 类型的 score,按 score 排序。底层编码在元素少(默认 <128 个)且值小(默认 <64 字节)时使用 listpack,否则使用 skiplist + dict 组合。

  • listpack:按 score 顺序存储,插入/删除需移动数据,适合小规模场景。
  • skiplist + dict:skiplist 维护有序性(O(log n) 范围查询),dict 提供 O(1) 元素查找,两者共享内存。

适用场景:

  • 排行榜(实时更新分数,ZRANGE 获取 Top N)。
  • 延迟队列(score 作为时间戳,ZRANGEBYSCORE 获取到期任务)。
  • 调度平台索引选择(按优先级或时间排序的任务列表)。
# 示例:游戏排行榜
ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
ZREVRANGE leaderboard 0 2 WITHSCORES
避坑指南: Sorted Set 的 skiplist 层高是概率性生成(最大 32 层),极端情况下可能导致查询退化为 O(n)。调度平台中若元素量级超过百万,建议评估 ZRANK 性能或使用分片策略。

2.7 数据结构选择决策树

以下 SVG 图展示了在调度平台索引选择场景中,如何根据数据特征快速确定 Redis 数据结构:

Redis 数据结构选择决策树 数据特征分析 需要排序/范围查询? 无需排序/简单存取 Sorted Set 需要去重/集合运算? Set 需要对象字段? Hash String / List 决策要点: • 排行榜/延迟队列 → Sorted Set • 对象缓存 → Hash;简单值/计数器 → String;消息队列 → List

2.8 编码切换对性能的影响

编码切换是 Redis 自动完成的,但切换瞬间可能引起短暂阻塞(尤其是大对象从 listpack 转为 hashtable 时)。调度平台索引选择时,应通过 OBJECT ENCODING 命令监控当前编码,并预估数据增长趋势。

# 查看 key 的当前编码
OBJECT ENCODING user:1001
# 输出示例:listpack 或 hashtable

编码切换阈值(Redis 7 默认值):

结构 参数 默认阈值 建议调整场景
Hash hash-max-listpack-entries 512 对象字段超过 1000 时增大
Hash hash-max-listpack-value 64 字段值超过 128 字节时增大
Set set-max-intset-entries 512 整数集合较大时保持 intset
Sorted Set zset-max-listpack-entries 128 排行榜元素少时保持 listpack
Sorted Set zset-max-listpack-value 64 元素值较小时保持紧凑
实战建议: 在调度平台中,任务元数据(Hash)和优先级队列(Sorted Set)是高频操作。建议将 hash-max-listpack-entries 调整为 1000,zset-max-listpack-entries 调整为 256,以延迟编码切换,减少内存碎片。

2.9 内存优化对比

以下 SVG 图对比了五种结构在存储 1000 个元素时的内存占用(近似值,基于 Redis 7 64 位环境):

Redis 数据结构内存占用对比(1000 元素) 内存占用对比(1000 个元素,单位:KB) ~50 KB String ~30 KB Hash ~40 KB List ~8 KB Set ~40 KB Sorted Set 注:实际内存受元素大小、编码类型、Redis 版本影响。Set 在 intset 编码下最省内存,适合整数集合。

从图中可以看出,Set(intset 编码)在存储整数时内存效率最高,Hash(listpack 编码)次之。调度平台索引选择时,若任务 ID 为整数且需要去重,优先考虑 Set;若需存储任务属性,Hash 是更优选择。

2.10 总结与索引选择建议

Redis 数据结构的底层编码直接影响内存效率与操作性能。在调度平台索引选择中,遵循以下原则:

  • 排行榜/延迟队列:Sorted Set(skiplist + dict)。
  • 任务元数据:Hash(listpack 或 hashtable)。
  • 任务去重/标签:Set(intset 或 hashtable)。
  • 消息队列/任务流:List(quicklist)。
  • 计数器/简单缓存:String(int/embstr/raw)。

通过 OBJECT ENCODING 监控编码状态,结合业务数据规模调整阈值参数,可显著提升缓存命中率与系统吞吐量。