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 的 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,节省内存。
2.7 数据结构选型决策树
在搜索缓存闭环中,按以下决策路径选择数据结构:
- 是否需要排序? 是 → Sorted Set;否 → 下一步。
- 是否需要去重? 是 → Set;否 → 下一步。
- 是否存储结构化对象? 是 → Hash;否 → 下一步。
- 是否需要队列特性? 是 → 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,内存占用会显著增加。在缓存一致性方案中,建议提前评估数据规模,合理设置阈值参数,避免运行时编码切换带来的性能抖动。