2. Redis 核心数据结构复盘:String、List、Hash、Set、Sorted Set 的内部编码与选择依据

Redis 之所以能成为缓存与索引场景的首选,核心在于其丰富的数据结构。本章将深入复盘五种最常用的数据结构——String、List、Hash、Set、Sorted Set,重点剖析它们的内部编码机制(底层实现)以及在实际业务中的选择依据。理解这些细节,是后续优化连接池水位与缓存索引性能的基础。

2.1 数据结构总览与编码演进

Redis 为每种数据结构提供了多种内部编码(底层实现),以在不同数据规模下达到最优的内存效率与操作性能。Redis 3.2 之后引入了 quicklist 替代 ziplist 作为 List 的默认编码;Redis 7.0 引入了 listpack 逐步替代 ziplist。下表总结了当前主流版本(6.x/7.x)的编码对照:

数据结构 内部编码(底层实现) 触发条件 核心优势
String int / embstr / raw 整数用 int;短字符串(≤44 字节)用 embstr;长字符串用 raw 内存紧凑,整数运算高效
List quicklist(3.2+) / listpack(7.0+) 默认使用 quicklist;7.0 后小列表可退化为 listpack 双向链表与压缩列表的混合体,平衡内存与读写性能
Hash ziplist / hashtable 字段数 < 512 且每个字段值 < 64 字节时用 ziplist,否则用 hashtable 小 Hash 内存极省,大 Hash 读写 O(1)
Set intset / hashtable 所有元素为整数且数量 < 512 时用 intset,否则用 hashtable 整数集合内存紧凑,支持交并差运算
Sorted Set ziplist / skiplist + dict 元素数 < 128 且每个元素长度 < 64 字节时用 ziplist,否则用 skiplist 有序集合,支持范围查询与排名
编码切换是透明的:Redis 会根据数据规模自动升级编码(从紧凑型到通用型),但不会自动降级。删除部分元素后,编码不会回退到紧凑模式,这是设计上的取舍。

2.2 String:最基础的缓存单元

String 是 Redis 中最简单的数据结构,但内部编码却有三态:

  • int:存储整数(如 10086),直接使用 C 的 long 类型,无额外内存开销。
  • embstr:存储 ≤44 字节的字符串,Redis 对象与 SDS 结构体连续分配,一次内存分配,读取快。
  • raw:存储 >44 字节的字符串,对象与 SDS 分开分配,适合大文本或二进制数据。

选择依据:

  • 计数器、分布式锁、Session 令牌 → 用 String(int 或 embstr)。
  • 缓存 JSON 序列化对象 → 用 String(raw),注意控制单 key 大小,建议 < 10 KB。
  • 避免将大文本(如 HTML 片段)直接存入 String,会占用大量内存且影响网络传输。
实战建议:使用 SET key value EX 10 NX 实现分布式锁时,value 建议用随机字符串(embstr 编码),避免锁误删。

2.3 List:消息队列与时间线

List 在 Redis 3.2 之后使用 quicklist 编码,它是 ziplist 与双向链表的混合体。每个 quicklist 节点是一个 ziplist(或 listpack),既支持两端快速插入删除,又保持了内存紧凑。

选择依据:

  • 消息队列(生产者-消费者) → 用 List + LPUSH / RPOP 或 BRPOP 阻塞读取。
  • 最新动态/时间线(如用户 Feed) → 用 LPUSH + LTRIM 控制长度。
  • 避免在长 List 中间位置进行 LINDEX 或 LSET 操作,时间复杂度 O(N)。
避坑:不要将 List 当作无限增长的日志存储。建议设置最大长度(如 1000 条),使用 LTRIM 定期裁剪,否则会撑爆内存并影响连接池响应时间。

2.4 Hash:对象缓存的首选

Hash 是存储对象属性的最佳选择。当字段数少且值小时,使用 ziplist 编码,内存效率极高;当数据量增大后自动切换为 hashtable,保证 O(1) 读写。

选择依据:

  • 用户信息、商品详情、配置项 → 用 Hash,每个字段对应一个属性。
  • 避免将整个对象序列化为 JSON 存入 String,使用 Hash 可以单独更新某个字段,节省网络带宽。
  • 当字段数超过 500 或单个字段值超过 64 字节时,ziplist 会转为 hashtable,此时内存节省效果减弱,但读写性能稳定。
# 推荐:使用 Hash 存储用户信息
HSET user:1001 name "张三" age 30 city "北京"
# 单独更新年龄
HSET user:1001 age 31
# 不推荐:整个对象序列化
SET user:1001 '{"name":"张三","age":30,"city":"北京"}'

2.5 Set:标签与去重

Set 用于存储不重复元素,支持高效的集合运算。当所有元素为整数且数量较少时,使用 intset 编码,内存占用极低;否则使用 hashtable。

选择依据:

  • 用户标签、兴趣分类 → 用 Set,支持 SADD / SREM 动态维护。
  • 去重统计(如 UV) → 用 SCARD 获取基数。
  • 交集/并集/差集运算 → 用 SINTER / SUNION / SDIFF,适合推荐系统。
  • 注意:当 Set 元素全为整数且数量 < 512 时,intset 编码内存极省;一旦插入非整数或超过阈值,立即转为 hashtable,无法回退。

2.6 Sorted Set:排行榜与延迟队列

Sorted Set 是 Redis 中最强大的数据结构之一,每个元素关联一个 double 类型的 score,按 score 排序。小数据量时使用 ziplist 编码,大数据量时使用 skiplist + dict 组合,兼顾范围查询与单点查询。

选择依据:

  • 排行榜(实时分数排名) → 用 ZADD 更新分数,ZREVRANGE 获取 Top N。
  • 延迟队列(定时任务) → 用 score 存储时间戳,ZRANGEBYSCORE 拉取到期任务。
  • 滑动窗口限流 → 用 ZREMRANGEBYSCORE 清理过期窗口。
  • 注意:ziplist 编码下,ZRANK 和 ZRANGE 是 O(N) 的;skiplist 编码下为 O(log N)。
性能关键:当 Sorted Set 元素数超过 128 或元素长度超过 64 字节时,编码从 ziplist 切换为 skiplist。切换后内存增加约 30%-50%,但范围查询性能大幅提升。如果业务中排行榜元素数长期在 100 以内,可以手动控制不触发切换,保持内存紧凑。

2.7 内部编码选择决策树

下图展示了 Redis 在写入数据时,如何根据数据特征自动选择内部编码的决策流程:

Redis 内部编码自动选择决策树 写入 Key-Value 判断数据类型 String 整数 → int 短串 → embstr 长串 → raw List quicklist / listpack Hash 字段<512 且 值<64B → ziplist 否则 → hashtable Set 全整数且<512 → intset 否则 → hashtable Sorted Set 元素<128 且 长度<64B → ziplist 否则 → skiplist+dict 编码切换规则 • 编码升级是自动的、单向的(紧凑 → 通用) • 删除元素后不会自动降级 • 可通过 OBJECT ENCODING key 查看当前编码

2.8 数据结构选择速查表

在实际架构设计中,根据业务场景选择合适的数据结构,能显著降低内存消耗并提升吞吐量。下表总结了常见场景的推荐选择:

业务场景 推荐数据结构 核心原因 避坑提示
计数器(PV/UV) String INCR/DECR 原子操作,int 编码内存极省 避免用 Hash 做计数器
对象缓存(用户/商品) Hash 支持字段级更新,小数据 ziplist 省内存 字段数过多时注意编码切换
消息队列 List LPUSH/RPOP 两端操作 O(1),支持阻塞读取 控制队列长度,避免内存溢出
排行榜 Sorted Set 按 score 排序,ZRANGE/ZREVRANGE 高效 小数据量注意 ziplist 性能
标签系统 Set 自动去重,支持交并差运算 大 Set 内存占用高,考虑 HyperLogLog
延迟队列 Sorted Set score 存时间戳,范围查询拉取到期任务 注意时间精度,避免 score 重复
分布式锁 String SET NX EX 原子操作,简单可靠 value 需唯一,防止误删
最新动态(Feed) List LPUSH + LTRIM 控制长度,读取快 不要用 Sorted Set 替代

2.9 编码查看与调优

Redis 提供了 OBJECT ENCODING 命令,可以实时查看 key 的当前内部编码,方便排查内存问题:

# 查看 key 的编码
127.0.0.1:6379> SET mykey "hello"
OK
127.0.0.1:6379> OBJECT ENCODING mykey
"embstr"

127.0.0.1:6379> HSET myhash name "alice" age 25
(integer) 2
127.0.0.1:6379> OBJECT ENCODING myhash
"ziplist"

# 当 Hash 字段数超过 512 后,编码自动切换
127.0.0.1:6379> OBJECT ENCODING myhash
"hashtable"
调优参数:可以通过 redis.conf 中的 hash-max-ziplist-entries、set-max-intset-entries、zset-max-ziplist-entries 等参数调整编码切换阈值,以适应不同业务的内存与性能需求。但建议保持默认值,除非你有明确的内存优化目标。

2.10 本章小结

  • Redis 五种核心数据结构各有 多种内部编码,自动在紧凑与通用之间切换。
  • String 适合计数器与简单缓存;Hash 是对象缓存的首选;List 适合队列与时间线;Set 用于标签与去重;Sorted Set 是排行榜与延迟队列的利器。
  • 编码切换是 单向升级,不会自动降级,设计时需考虑数据规模增长后的内存变化。
  • 使用 OBJECT ENCODING 命令可以实时监控编码状态,辅助调优。
  • 选择数据结构时,优先考虑 操作复杂度 与 内存效率 的平衡,避免大 Key 与全量扫描。