2. Redis核心数据结构:String、List、Hash、Set、Sorted Set的使用场景与底层原理

在构建搜索缓存闭环时,选择正确的数据结构直接决定了缓存的命中率、内存效率与操作复杂度。Redis 提供了五种核心数据结构,每种都有独特的底层编码与适用边界。本章从使用场景出发,深入剖析其底层原理,帮助你在缓存一致性方案中做出精准选型。

2.1 数据结构全景对比

数据结构 底层编码(Redis 7+) 时间复杂度(关键操作) 核心应用场景
String int / embstr / raw GET/SET: O(1) 缓存单个值、计数器、分布式锁
List quicklist(ziplist + linkedlist) LPUSH/RPOP: O(1);LINDEX: O(N) 消息队列、最新列表、时间线
Hash listpack / hashtable HGET/HSET: O(1);HGETALL: O(N) 对象缓存、用户属性、购物车
Set listpack / hashtable SADD/SISMEMBER: O(1);SMEMBERS: O(N) 标签系统、去重、共同好友
Sorted Set listpack / skiplist + hashtable ZADD: O(log N);ZRANGE: O(log N + M) 排行榜、延迟队列、范围查询
选型原则: 优先用 Hash 缓存结构化对象,用 Sorted Set 处理带权重的有序数据,用 String 处理简单键值。List 和 Set 在搜索缓存中多用于辅助索引和去重队列。

2.2 String:最基础的缓存单元

使用场景

  • 缓存搜索结果片段: 将序列化后的搜索结果 JSON 直接存入 String,key 为搜索关键词的 MD5 或业务 ID。
  • 计数器: 搜索频次统计、点击量、限流令牌桶。
  • 分布式锁: SETNX + EXPIRE 实现互斥锁,防止缓存击穿。

底层原理

String 的编码根据存储内容动态切换:

  • int: 当值为整数且可被 64 位有符号整数表示时,直接存储在 redisObject 的 ptr 中,无需额外内存分配。
  • embstr: 当字符串长度 ≤ 44 字节(Redis 7+ 为 44,早期版本为 39),使用连续内存块一次性分配,减少内存碎片。
  • raw: 当字符串长度 > 44 字节,使用 SDS(Simple Dynamic String)结构,支持动态扩容和二进制安全。
// SDS 结构示意(Redis 7+)
struct sdshdr {
    uint8_t len;        // 已使用长度
    uint8_t alloc;      // 总分配长度
    unsigned char flags; // 类型标记
    char buf[];         // 字节数组
};
缓存一致性注意: String 的 SET 操作是原子性的,但涉及多个键的更新时,需要使用 Lua 脚本或事务保证原子性,否则会出现缓存与数据库不一致。

2.3 List:有序队列与时间线

使用场景

  • 搜索历史记录: 每个用户一个 List,LPUSH 最新搜索词,LTRIM 保留最近 N 条。
  • 消息队列: 生产者 LPUSH,消费者 RPOP 或 BRPOP 阻塞读取。
  • 最新动态列表: 存储最近更新的文档 ID 列表,用于缓存预热。

底层原理

Redis 3.2 之后 List 统一使用 quicklist 结构,它是 ziplist 与双向链表的混合体:

  • 每个 quicklist 节点是一个 ziplist(紧凑压缩列表),存储多个元素。
  • ziplist 大小可通过 list-max-ziplist-size 配置,默认 8KB。
  • 当 ziplist 过大或元素长度不均时,自动拆分为多个节点,保证内存紧凑与操作效率的平衡。
List 底层结构:quicklist quicklistNode quicklistNode quicklistNode ziplist ziplist ziplist e1 e2 e3 e4 e5 e6 e7 每个 quicklistNode 内部是一个 ziplist, ziplist 存储连续的小元素,减少指针开销。
避坑: List 的 LINDEX 操作是 O(N),不适合随机访问。如果需要频繁按索引读取,考虑使用 Sorted Set 或单独维护索引。

2.4 Hash:结构化对象缓存

使用场景

  • 缓存搜索结果对象: 每个文档 ID 作为 key,文档字段(标题、摘要、得分)作为 field-value 对。
  • 用户画像缓存: 用户 ID 为 key,属性(年龄、偏好、历史行为)为 field。
  • 购物车: 用户 ID 为 key,商品 ID 为 field,数量为 value。

底层原理

Hash 的编码选择取决于字段数量和单个字段大小:

  • listpack(Redis 7+ 替代 ziplist): 当字段数 < 512 且每个字段长度 < 64 字节时使用。listpack 是紧凑的连续内存结构,每个元素通过长度前缀编码,无需指针。
  • hashtable: 当条件超出阈值时,升级为字典结构,使用链地址法解决哈希冲突,rehash 采用渐进式策略。
// Hash 操作示例:缓存搜索结果
HSET doc:1001 title "Redis缓存一致性实战" score 9.5 author "资深架构师"
HGET doc:1001 title
// 返回: "Redis缓存一致性实战"
缓存一致性技巧: 使用 Hash 缓存对象时,更新单个字段只需 HSET,避免像 String 那样整体序列化/反序列化,减少网络开销和并发冲突概率。

2.5 Set:去重与集合运算

使用场景

  • 搜索标签系统: 每个标签对应一个 Set,存储包含该标签的文档 ID。
  • 去重队列: 记录已处理的任务 ID,防止重复消费。
  • 共同好友/交集查询: SINTER 计算两个集合的交集,用于推荐系统。

底层原理

  • listpack: 当集合元素全为整数且数量 < 512 时使用,紧凑存储。
  • hashtable: 其他情况使用字典,value 为 NULL,key 为元素本身,实现 O(1) 的添加、删除、判重。
搜索缓存应用: 用 Set 存储“已缓存的关键词集合”,在缓存写入前先 SISMEMBER 检查,避免重复缓存相同关键词,节省内存。

2.6 Sorted Set:带权重的有序集合

使用场景

  • 搜索热词排行榜: 搜索词为 member,搜索次数为 score,ZINCRBY 实时更新。
  • 延迟队列: 任务执行时间戳为 score,ZRANGEBYSCORE 获取到期任务。
  • 分页缓存: 按得分排序的搜索结果,ZRANGE 实现高效分页。

底层原理

Sorted Set 使用 skiplist + hashtable 双结构:

  • skiplist: 维护 member 按 score 有序排列,支持 O(log N) 的插入、删除、范围查询。跳表通过多层索引加速查找,层数由随机函数决定(最大 32 层)。
  • hashtable: 维护 member 到 score 的映射,支持 O(1) 的 ZSCORE 操作。
  • listpack: 当元素数量 < 128 且所有元素长度 < 64 字节时,使用 listpack 替代 skiplist,节省内存。
Sorted Set 底层结构:skiplist + hashtable head A:10 D:40 G:70 head A:10 C:30 F:60 H:80 head A:10 B:20 C:30 D:40 E:50 hashtable(member → score) A → 10 B → 20 C → 30 D → 40 skiplist 提供有序范围查询(ZRANGE、ZRANK), hashtable 提供 O(1) 的 ZSCORE 查询。 两者通过 member 指针关联,保证数据一致性。

2.7 数据结构选型决策树

在搜索缓存闭环中,按以下决策路径选择数据结构:

  1. 是否需要排序? 是 → Sorted Set;否 → 下一步。
  2. 是否需要去重? 是 → Set;否 → 下一步。
  3. 是否存储结构化对象? 是 → Hash;否 → 下一步。
  4. 是否需要队列特性? 是 → List;否 → String。
实战建议: 在搜索缓存中,80% 的场景使用 Hash 和 Sorted Set。Hash 用于缓存搜索结果对象,Sorted Set 用于热词排行和分页索引。String 仅用于简单计数器或锁。List 和 Set 多用于辅助队列和去重。

2.8 内存优化要点

数据结构 内存优化策略 配置参数
String 使用 embstr 编码(≤44 字节);避免大 Value 无
List 控制 ziplist 大小;使用 LPUSH + LTRIM 限制长度 list-max-ziplist-size
Hash 保持字段数 < 512,字段长度 < 64 字节以使用 listpack hash-max-listpack-entries、hash-max-listpack-value
Set 使用整数集合编码;控制集合大小 set-max-listpack-entries
Sorted Set 保持元素数 < 128,元素长度 < 64 字节以使用 listpack zset-max-listpack-entries、zset-max-listpack-value
注意: 当数据量超过 listpack/ziplist 阈值时,编码会升级为 hashtable 或 skiplist,内存占用会显著增加。在缓存一致性方案中,建议提前评估数据规模,合理设置阈值参数,避免运行时编码切换带来的性能抖动。