2. Redis 核心数据结构:String、List、Set、ZSet、Hash 及其应用场景
Redis 之所以能成为缓存与限流场景的首选,其核心优势在于提供了丰富且高效的数据结构。本章将系统回顾五种基础数据结构,重点剖析其底层实现、操作特性以及在限流与缓存一致性实战中的典型用法。
2.1 数据结构全景对比
在深入每个结构之前,先通过一张对比表建立全局认知:
| 数据结构 | 底层编码 | 最大元素数 | 有序性 | 唯一性 | 核心操作时间复杂度 |
|---|---|---|---|---|---|
| String | int / embstr / raw | 512 MB | — | — | GET/SET: O(1) |
| List | quicklist (ziplist + linkedlist) | 2^32 - 1 | 插入顺序 | 否 | LPUSH/RPOP: O(1) LINDEX: O(n) |
| Set | intset / hashtable | 2^32 - 1 | 否 | 是 | SADD/SISMEMBER: O(1) |
| ZSet | ziplist / skiplist + dict | 2^32 - 1 | Score 排序 | 是 | ZADD: O(log n) ZRANGE: O(log n + m) |
| Hash | ziplist / hashtable | 2^32 - 1 (每个 field) | 否 | field 唯一 | HGET/HSET: O(1) |
实战提示: 在限流场景中,ZSet 的 score 排序特性是实现滑动窗口算法的基石;而 Hash 结构则常用于缓存一致性中的实体对象存储,避免序列化整个对象的开销。
2.2 String — 最基础的原子操作单元
String 是 Redis 最基础的数据结构,value 可以是字符串、整数或浮点数。其底层编码根据数据长度动态切换:
- int:当 value 为 64 位有符号整数时使用,直接进行原子自增/自减。
- embstr:当字符串长度 ≤ 44 字节时,Redis 使用连续内存块存储,减少内存碎片。
- raw:当字符串长度 > 44 字节时,使用 SDS(Simple Dynamic String)结构,支持动态扩容。
2.2.1 核心命令与限流应用
# 基础操作
SET user:1001 "{\"name\":\"alice\",\"level\":5}"
GET user:1001
# 原子计数(限流计数器核心)
INCR api:limit:user:1001
EXPIRE api:limit:user:1001 60
# 带过期时间的 SET(缓存一致性常用)
SET cache:key "value" EX 300 NX
限流场景: 使用
INCR + EXPIRE 实现固定窗口计数器。每次请求先 INCR,若返回值超过阈值则拒绝。注意:需在 INCR 后判断是否为 1 来设置过期时间,避免每次重置。
2.3 List — 消息队列与时间线
List 底层使用 quicklist 结构,它是 ziplist 与双向链表的混合体。每个 quicklist 节点是一个 ziplist,在内存紧凑性与操作效率之间取得平衡。
2.3.1 典型操作模式
# 右侧入队,左侧出队(FIFO 队列)
LPUSH task:queue "job:001"
RPOP task:queue
# 阻塞式弹出(实现可靠消息消费)
BRPOP task:queue 5
# 获取范围(分页查询)
LRANGE user:timeline 0 9
2.3.2 应用场景
- 异步任务队列:LPUSH + BRPOP 实现生产者-消费者模式。
- 最新动态列表:LPUSH 插入新内容,LTRIM 裁剪保留最近 N 条。
- 限流日志缓冲区:将请求日志写入 List,后台异步分析。
避坑指南: List 的
LINDEX 操作时间复杂度为 O(n),不适合随机访问。若需要频繁按索引查询,请考虑使用 ZSet 或 Hash。
2.4 Set — 去重与集合运算
Set 底层使用 intset(当所有元素为整数且数量较少时)或 hashtable。intset 使用有序数组存储,查找效率 O(log n);当元素类型不统一或数量超过阈值时自动升级为 hashtable。
2.4.1 集合运算实战
# 添加与判断
SADD user:1001:tags "redis" "cache" "限流"
SISMEMBER user:1001:tags "redis"
# 集合运算(用户画像、权限交集)
SINTER user:1001:tags user:1002:tags # 共同标签
SUNION user:1001:tags user:1002:tags # 全部标签
SDIFF user:1001:tags user:1002:tags # 差异标签
2.4.2 限流场景:去重计数器
使用 SADD 记录某时间窗口内的唯一用户 ID,SCARD 获取去重后的请求数,适用于“同一用户多次请求只计一次”的限流策略。
2.5 ZSet — 排序集合与滑动窗口
ZSet 是限流场景中最重要的数据结构。底层使用 skiplist + dict 组合:skiplist 保证按 score 有序,dict 保证 O(1) 的成员查找。
2.5.1 滑动窗口限流实现
# 记录请求时间戳
ZADD rate:limit:api:user:1001 1680000000 "req:uuid1"
# 移除窗口外的旧记录
ZREMRANGEBYSCORE rate:limit:api:user:1001 0 1679999400
# 统计当前窗口请求数
ZCARD rate:limit:api:user:1001
2.5.2 排行榜与优先级队列
- 实时排行榜:ZINCRBY 更新分数,ZREVRANGE 获取 Top N。
- 延迟队列:以执行时间戳为 score,定时轮询 ZRANGEBYSCORE 获取到期任务。
- 权重限流:不同 API 设置不同权重 score,累计权重超过阈值则限流。
性能要点: ZSet 的
ZADD 时间复杂度为 O(log n),当集合规模达到百万级时,单次插入约 10-20 微秒。滑动窗口限流建议配合 ZREMRANGEBYSCORE 定期清理过期数据,避免 ZSet 无限膨胀。
2.6 Hash — 对象存储与缓存一致性
Hash 底层使用 ziplist(当 field-value 对较少且值较短时)或 hashtable。ziplist 将 field 和 value 连续存储,节省内存;hashtable 提供 O(1) 的随机访问。
2.6.1 对象缓存模式
# 存储用户对象(避免序列化整个对象)
HSET user:1001 name "alice" age 30 level 5
HGET user:1001 name
HGETALL user:1001
# 原子增减(缓存一致性中的计数器)
HINCRBY user:1001 score 10
2.6.2 缓存一致性中的应用
- 部分更新:只更新 Hash 中的某个 field,避免读取整个缓存对象再写回。
- 字段级过期:结合
HEXISTS和业务逻辑实现字段级 TTL 判断。 - 数据库行映射:将数据库一行记录映射为一个 Hash key,field 对应列名,减少缓存键数量。
实战对比: 使用 String 存储 JSON 对象时,更新单个字段需要先 GET 反序列化 → 修改 → SET 序列化,存在并发覆盖风险。使用 Hash 则可以直接
HSET 更新单个 field,天然支持部分更新,更适合缓存一致性场景。
2.7 数据结构选择决策树
下面的 SVG 图展示了在限流与缓存一致性场景中,如何根据需求选择合适的数据结构:
2.8 数据结构与限流算法映射
下表总结了不同限流算法对应的 Redis 数据结构选择:
| 限流算法 | 推荐数据结构 | 核心命令 | 优势 | 局限 |
|---|---|---|---|---|
| 固定窗口 | String | INCR + EXPIRE | 实现简单,性能极高 | 窗口边界流量突刺 |
| 滑动窗口 | ZSet | ZADD + ZREMRANGEBYSCORE + ZCARD | 精确控制时间窗口 | 内存占用随请求量增长 |
| 令牌桶 | String + List | GET/SET + LPUSH/RPOP | 支持突发流量 | 需客户端维护令牌生成逻辑 |
| 漏桶 | List | LLEN + LPUSH + RPOP | 平滑输出流量 | 无法应对突发 |
| 去重计数 | Set | SADD + SCARD | 天然去重 | 内存占用高 |
2.9 内存优化与编码转换
理解底层编码有助于在限流场景中优化内存:
- ziplist 阈值:Hash 和 ZSet 在元素较少时使用 ziplist,可通过
hash-max-ziplist-entries和zset-max-ziplist-entries配置。超过阈值后自动转换为 hashtable 或 skiplist。 - intset 升级:Set 中所有元素为整数且数量不超过
set-max-intset-entries时使用 intset,内存效率极高。 - 监控命令:使用
DEBUG OBJECT key查看底层编码类型,MEMORY USAGE key查看实际内存占用。
限流场景内存警告: 滑动窗口限流使用 ZSet 存储每个请求的时间戳,若 QPS 为 1000、窗口为 1 分钟,则每个用户需存储 60,000 个元素。务必设置合理的过期策略或定期清理,避免内存泄漏。