2. Redis 核心数据结构:String、List、Set、ZSet、Hash 的底层实现与适用场景
Redis 之所以能成为缓存与索引领域的首选,核心在于其丰富的数据结构。每种结构都对应着特定的底层编码与算法,理解这些实现是精准建模领域边界的前提。本章将逐一剖析五种核心类型的底层机制与最佳实践。
2.1 数据结构总览与编码选择
Redis 内部为每种数据类型维护了多种底层编码(encoding),根据存储元素的大小、数量动态切换,以平衡内存与性能。下表展示了各类型的默认编码与切换条件:
| 数据类型 | 底层编码(按切换顺序) | 触发切换的关键阈值 |
|---|---|---|
| String | int → embstr → raw | 整数用 int;字符串 ≤ 44 字节用 embstr;> 44 字节用 raw |
| List | ziplist → linkedlist / quicklist | 元素数 > 512 或单个元素 > 64 字节时升级 |
| Set | intset → hashtable | 元素全为整数且数量 ≤ 512 时用 intset,否则用 hashtable |
| ZSet | ziplist → skiplist + dict | 元素数 > 128 或单个元素 > 64 字节时升级 |
| Hash | ziplist → hashtable | 字段数 > 512 或字段/值长度 > 64 字节时升级 |
2.2 String:最基础的缓存单元
String 是 Redis 的基石,最大存储 512 MB。其底层采用 SDS(Simple Dynamic String)结构,相比 C 字符串具备 O(1) 长度获取、二进制安全、预分配空间等优势。
- 适用场景: 缓存单个值(如用户 Token、配置项)、计数器(INCR/DECR)、分布式锁(SETNX)、限流令牌桶。
- 领域边界映射: 适合表示不可变的值对象或标量属性,如
user:1001:email。
# 缓存用户会话
SET session:abc123 '{"userId": 1001, "role": "admin"}' EX 3600
# 原子计数器
INCR article:readcount:42
# 分布式锁
SET lock:order:2024 nx ex 10
2.3 List:有序队列与消息缓冲
List 底层在 Redis 3.2 之后统一采用 quicklist(由多个 ziplist 节点组成的双向链表),兼顾了内存碎片率与两端操作性能。
- 适用场景: 消息队列(LPUSH + BRPOP)、时间线(Timeline)、最新评论列表。
- 领域边界映射: 适合表示有序的聚合根内部事件流或任务队列,注意 List 不支持随机访问,不适合做索引。
# 生产者
LPUSH task:queue "job:123"
# 消费者(阻塞读取)
BRPOP task:queue 0
2.4 Set:去重集合与标签系统
Set 底层使用 intset(整数集合)或 hashtable(字典)。intset 采用二分查找实现 O(log n) 操作,升级为 hashtable 后提供 O(1) 的增删查。
- 适用场景: 用户标签、好友关系、抽奖去重、共同关注(SINTER)。
- 领域边界映射: 适合表示值对象集合或枚举类型,如
user:1001:tags存储兴趣标签。
# 添加标签
SADD user:1001:tags "tech" "redis" "ddd"
# 求共同标签
SINTER user:1001:tags user:1002:tags
2.5 ZSet:排序索引与排行榜
ZSet 是 Redis 最强大的索引结构,底层结合 skiplist(跳表) 与 dict(字典)。跳表以 O(log n) 实现范围查询与排序,字典提供 O(1) 的成员分数查询。
- 适用场景: 排行榜、延迟队列(按时间戳排序)、分页索引、带权重的推荐列表。
- 领域边界映射: 最适合构建领域层的排序索引,如
order:score:2024按金额排序的订单索引。
# 添加订单(分数为金额)
ZADD order:score:2024 299.99 "order:1001"
ZADD order:score:2024 150.00 "order:1002"
# 获取金额前 10 的订单
ZREVRANGE order:score:2024 0 9 WITHSCORES
2.6 Hash:对象映射与领域聚合
Hash 底层使用 ziplist(压缩列表)或 hashtable(字典)。ziplist 将键值对连续存储,节省内存但查询为 O(n);升级为 hashtable 后提供 O(1) 字段级操作。
- 适用场景: 存储对象属性(如用户信息、商品详情)、计数器组、配置映射。
- 领域边界映射: 最贴近 DDD 中的聚合根(Aggregate Root)存储,每个字段对应一个值对象或基本属性。
# 存储用户聚合根
HSET user:1001 name "Alice" age 30 email "alice@example.com"
# 获取单个字段
HGET user:1001 email
# 获取所有字段
HGETALL user:1001
2.7 底层实现对比与选型决策
以下 SVG 图展示了五种数据结构在领域建模中的典型映射关系,帮助你在设计缓存索引时快速决策:
2.8 编码切换对性能的影响
理解编码切换的阈值对于设计缓存索引至关重要。以下场景需要特别关注:
| 场景 | 编码状态 | 性能特征 | 建议 |
|---|---|---|---|
| 小规模 Hash(< 512 字段) | ziplist | 内存紧凑,查询 O(n) 但 n 小可忽略 | 适合存储聚合根 |
| 大规模 Hash(> 512 字段) | hashtable | 内存开销增加,查询 O(1) | 考虑拆分为多个小 Hash |
| ZSet 排行榜(< 128 元素) | ziplist | 插入/删除 O(n),但 n 小 | 适合小范围排名 |
| ZSet 排行榜(> 128 元素) | skiplist | 插入/删除 O(log n),范围查询高效 | 适合大规模排序索引 |
2.9 领域驱动设计中的数据结构选择
在 DDD 实践中,每种 Redis 数据结构对应着不同的领域概念:
- 值对象(Value Object): 使用 String 或 Hash 存储不可变属性,如
Address对象。 - 实体(Entity): 使用 Hash 存储可变属性,配合 String 存储唯一标识。
- 聚合根(Aggregate Root): 使用 Hash 存储主体属性,ZSet 维护子实体排序索引。
- 领域事件(Domain Event): 使用 List 或 Stream 存储事件流。
- 规格(Specification): 使用 Set 存储满足条件的 ID 集合。