2. Redis 核心数据结构回顾:String、Hash、List、Set、Sorted Set 在缓存场景中的典型用法

在构建搜索缓存闭环时,Redis 的五种核心数据结构是承载缓存数据的基石。每种结构都有其独特的适用场景,选错结构会导致性能下降或代码复杂度飙升。本节从缓存实战角度,逐一回顾它们的特性与典型用法。

2.1 数据结构选型速查表

数据结构 底层编码 缓存场景核心能力 典型 TTL 策略
String int / embstr / raw 单值缓存、计数器、分布式锁 固定过期或滑动过期
Hash ziplist / hashtable 对象字段级缓存、部分更新 整体过期或逐 field 管理
List quicklist 消息队列、时间线、分页缓存 通常不设 TTL,用 LTRIM 控制长度
Set intset / hashtable 标签系统、去重、集合运算 整体过期或定期清理
Sorted Set ziplist / skiplist 排行榜、范围查询、滑动窗口限流 按 score 时间戳实现自动淘汰

2.2 String —— 最基础的缓存单元

String 是 Redis 最直观的结构,value 最大 512MB。在缓存场景中,它承担了 键值对缓存 和 计数器 两大职责。

实战要点: 使用 SET key value EX NX 实现分布式锁;使用 INCR/DECR 做访问计数;使用 GETSET 实现原子性更新。
// 缓存搜索结果摘要(JSON 序列化)
SET search:hotel:1001 '{"id":1001,"name":"希尔顿","price":899}' EX 300

// 原子计数器:记录搜索频次
INCR search:count:2025-03-20

// 分布式锁:防止缓存击穿
SET lock:rebuild:hotel NX EX 10

2.3 Hash —— 对象级缓存的利器

Hash 允许对对象的多个字段进行独立读写,适合缓存 结构化数据。相比 String 序列化整个对象,Hash 在 部分字段更新 时节省大量网络带宽。

操作 命令示例 缓存场景
写入对象 HSET hotel:1001 name "希尔顿" price 899 缓存酒店基本信息
读取单字段 HGET hotel:1001 price 仅获取价格,避免传输整个对象
批量读取 HMGET hotel:1001 name price 获取部分字段组合
字段自增 HINCRBY hotel:1001 views 1 对象级计数器
选型建议: 当缓存对象字段超过 10 个且频繁更新其中 1-2 个字段时,Hash 比 String 节省约 60% 的序列化开销。

2.4 List —— 有序队列与分页缓存

List 基于双向链表实现,支持 左进右出 和 范围查询。在搜索缓存中,常用于存储 搜索结果 ID 列表 或 消息队列。

// 缓存搜索结果 ID 列表(分页友好)
LPUSH search:result:query "1001" "1002" "1003"
LTRIM search:result:query 0 99   // 只保留前 100 条

// 分页读取
LRANGE search:result:query 0 9   // 第 1 页
LRANGE search:result:query 10 19 // 第 2 页
避坑提示: List 的 LINDEX 和 LSET 操作时间复杂度为 O(N),大数据量下避免频繁随机访问。分页场景优先使用 LRANGE。

2.5 Set —— 标签与去重利器

Set 提供 自动去重 和 集合运算(交集、并集、差集)。在搜索缓存中,适合存储 标签索引 和 用户行为去重。

// 为酒店打标签
SADD tag:泳池 1001 1002 1005
SADD tag:健身房 1001 1003 1004

// 查询同时有泳池和健身房的酒店
SINTER tag:泳池 tag:健身房
// 结果:1001

// 用户已浏览去重
SADD user:123:viewed 1001 1002 1003
SCARD user:123:viewed  // 统计已浏览数量

2.6 Sorted Set —— 排序与范围查询之王

Sorted Set 每个元素关联一个 score,按 score 排序。在搜索缓存中,它是 排行榜、范围查询 和 滑动窗口限流 的核心结构。

场景 命令示例 score 含义
搜索热词排行 ZINCRBY hot:search:day 1 "Redis" 搜索次数
按价格范围查询 ZRANGEBYSCORE hotel:price 500 1000 酒店价格
滑动窗口限流 ZREMRANGEBYSCORE user:req:123 0 (now-60s 请求时间戳
分页获取排名 ZREVRANGE hot:search:day 0 9 WITHSCORES 按次数降序
// 滑动窗口限流:1 分钟内最多 10 次请求
ZREMRANGEBYSCORE user:req:123 0 (1710800000
ZCARD user:req:123
// 如果 < 10,则允许请求并添加当前时间戳
ZADD user:req:123 1710800123 "req:1710800123"

2.7 数据结构选择决策流程

缓存数据结构选择决策流程 缓存需求 是否需要排序? Sorted Set 排行榜 / 范围查询 / 限流 是否需要去重? Set 标签 / 去重 / 集合运算 是否对象多字段? Hash 对象缓存 / 部分更新 是否队列/分页? List 消息队列 / 分页缓存 String 单值缓存 / 计数器 / 锁

2.8 缓存场景中的内存优化建议

  • String 压缩: 对 JSON 字符串使用 gzip 压缩后再存入,可减少 60-80% 内存占用。
  • Hash 编码控制: 当 field 数量小于 512 且每个 value 小于 64 字节时,Redis 自动使用 ziplist 编码,内存效率极高。
  • Set 整数优化: 当所有元素为整数且数量小于 512 时,使用 intset 编码,内存仅为 hashtable 的 1/5。
  • Sorted Set 压缩: 合理设置 zset-max-ziplist-entries 和 zset-max-ziplist-value,避免 skiplist 的额外指针开销。
  • 过期策略: 为每个缓存键设置合理的 TTL,避免冷数据长期占用内存。
核心原则: 数据结构的选择应基于 访问模式 而非数据本身。同一个对象,如果频繁读取全部字段,用 String;如果频繁更新部分字段,用 Hash。没有银弹,只有匹配。