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 | 排序、范围查询、排行榜 |
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
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
2.7 数据结构选择决策树
以下 SVG 图展示了在调度平台索引选择场景中,如何根据数据特征快速确定 Redis 数据结构:
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 | 元素值较小时保持紧凑 |
2.9 内存优化对比
以下 SVG 图对比了五种结构在存储 1000 个元素时的内存占用(近似值,基于 Redis 7 64 位环境):
从图中可以看出,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 监控编码状态,结合业务数据规模调整阈值参数,可显著提升缓存命中率与系统吞吐量。