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 图展示了在限流与缓存一致性场景中,如何根据需求选择合适的数据结构:

数据结构选择决策树(限流 & 缓存一致性) 业务需求 限流场景 缓存一致性 固定窗口 滑动窗口 String (INCR) ZSet (ZADD) 对象缓存 计数器 Hash (HSET) String (INCR) 选择建议 • 限流首选:固定窗口用 String,滑动窗口用 ZSet • 缓存一致性:对象存储用 Hash,计数器用 String • 去重场景:Set 的 SADD + SCARD 实现唯一计数

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 个元素。务必设置合理的过期策略或定期清理,避免内存泄漏。