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 | 有序集合,支持范围查询与排名 |
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)。
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)。
2.7 内部编码选择决策树
下图展示了 Redis 在写入数据时,如何根据数据特征自动选择内部编码的决策流程:
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"
hash-max-ziplist-entries、set-max-intset-entries、zset-max-ziplist-entries 等参数调整编码切换阈值,以适应不同业务的内存与性能需求。但建议保持默认值,除非你有明确的内存优化目标。
2.10 本章小结
- Redis 五种核心数据结构各有 多种内部编码,自动在紧凑与通用之间切换。
- String 适合计数器与简单缓存;Hash 是对象缓存的首选;List 适合队列与时间线;Set 用于标签与去重;Sorted Set 是排行榜与延迟队列的利器。
- 编码切换是 单向升级,不会自动降级,设计时需考虑数据规模增长后的内存变化。
- 使用
OBJECT ENCODING命令可以实时监控编码状态,辅助调优。 - 选择数据结构时,优先考虑 操作复杂度 与 内存效率 的平衡,避免大 Key 与全量扫描。