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 字节时升级
设计启示: 在领域驱动设计中,值对象(Value Object)的边界往往对应小规模 Hash 或 ZSet。利用 ziplist 编码可以在内存紧凑性与访问速度之间取得理想平衡,尤其适合高频读取的缓存索引。

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
跳表 vs B+ 树: 跳表实现简单、支持并发修改、范围查询性能稳定(O(log n)),适合内存数据库。B+ 树更适合磁盘 I/O 场景(如 MySQL)。在 Redis 中,跳表配合字典实现了 ZSet 的 O(1) 分数查询与 O(log n) 范围操作。

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 图展示了五种数据结构在领域建模中的典型映射关系,帮助你在设计缓存索引时快速决策:

Redis 数据结构与领域边界映射 领域层(Domain) String SDS 动态字符串 List Quicklist Set Intset / Hashtable ZSet Skiplist + Dict Hash Ziplist / Hashtable 适用场景映射: • String → 值对象、计数器、分布式锁 • List → 事件流、消息队列、时间线 • Set → 标签集合、去重、共同关注 • ZSet → 排序索引、排行榜、延迟队列 • Hash → 聚合根、对象属性、配置映射 选型决策流程 1. 是否需要排序? → 是 → ZSet 2. 是否需要去重? → 是 → Set 3. 是否需要字段级操作? → 是 → Hash 4. 是否需要队列/栈? → 是 → List 5. 以上都不是 → String

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),范围查询高效 适合大规模排序索引
避坑指南: 不要将 Hash 的字段数设计在 512 附近徘徊,频繁的编码切换会导致性能抖动。建议领域建模时明确小聚合(< 500 字段)与大聚合(> 1000 字段)的边界,并提前规划拆分策略。

2.9 领域驱动设计中的数据结构选择

在 DDD 实践中,每种 Redis 数据结构对应着不同的领域概念:

  • 值对象(Value Object): 使用 String 或 Hash 存储不可变属性,如 Address 对象。
  • 实体(Entity): 使用 Hash 存储可变属性,配合 String 存储唯一标识。
  • 聚合根(Aggregate Root): 使用 Hash 存储主体属性,ZSet 维护子实体排序索引。
  • 领域事件(Domain Event): 使用 List 或 Stream 存储事件流。
  • 规格(Specification): 使用 Set 存储满足条件的 ID 集合。