2. Redis 核心数据结构回顾:String、List、Set、ZSet、Hash 在限流与缓存中的应用场景

在构建限流与缓存闭环系统时,Redis 的五种核心数据结构是操作原子。每种结构在延迟、内存效率与操作语义上各有取舍,直接决定了限流算法的实现成本与缓存策略的命中率。本节从底层特性出发,逐一拆解每种结构在限流与缓存中的典型应用模式。

2.1 数据结构全景对比

结构 底层编码 时间复杂度(关键操作) 限流场景 缓存场景
String int / embstr / raw O(1) 读写 令牌桶剩余令牌、计数器 热点 KV、Session、配置
List quicklist O(1) 头尾操作 滑动窗口请求日志 消息队列、最新列表
Set intset / hashtable O(1) 增删查 去重限流、黑名单 标签索引、用户集合
ZSet ziplist / skiplist O(log N) 范围查询 滑动窗口时间戳排序 排行榜、热数据排序
Hash ziplist / hashtable O(1) 字段读写 多维度限流计数 对象缓存、部分更新
选型原则:限流场景优先关注 原子性 与 时间窗口裁剪能力;缓存场景优先关注 内存密度 与 部分更新效率。ZSet 的 skiplist 在百万级成员下仍能保持稳定 O(log N) 范围删除,是滑动窗口限流的事实标准。

2.2 String —— 计数器与令牌桶

String 是 Redis 最基础的结构,支持 INCR、DECR、INCRBYFLOAT 等原子操作,适合实现固定窗口计数器与令牌桶剩余令牌管理。

限流应用:固定窗口计数器

-- 每秒最多 10 次请求
INCR counter:user:1001
EXPIRE counter:user:1001 1
-- 若返回值 > 10,则拒绝

缓存应用:热点 KV 与序列化对象

SET user:1001:profile '{"name":"alice","level":5}' EX 3600
GET user:1001:profile
陷阱:固定窗口计数器在窗口边界存在突发流量穿透风险。例如第 59 秒 10 次请求 + 第 0 秒 10 次请求,实际 2 秒内通过 20 次。解决方案是改用 ZSet 滑动窗口或令牌桶算法。

2.3 List —— 请求日志队列与 LRU 淘汰辅助

List 的 LPUSH + LTRIM 组合可以维护一个固定长度的请求时间戳队列,用于实现基于 FIFO 的滑动窗口限流。

限流应用:FIFO 滑动窗口

-- 记录当前毫秒时间戳
LPUSH request_log:user:1001 1710000000123
LTRIM request_log:user:1001 0 99
-- 检查队列长度,超过 100 则拒绝
LLEN request_log:user:1001

缓存应用:最新 N 条消息

LPUSH recent_news:tech "Redis 7.4 released"
LTRIM recent_news:tech 0 49
LRANGE recent_news:tech 0 9
性能特征:List 的头尾操作时间复杂度 O(1),但 LTRIM 在删除大量元素时可能阻塞。建议窗口大小控制在 1000 以内,或改用 ZSet 的 ZREMRANGEBYSCORE 异步裁剪。

2.4 Set —— 去重限流与标签缓存

Set 天然支持去重与集合运算,适合做基于用户 ID 或 IP 的去重限流,以及缓存中的标签反向索引。

限流应用:去重黑名单

SADD blacklist:ip 192.168.1.1
SISMEMBER blacklist:ip 192.168.1.1  -- 返回 1 则拦截

缓存应用:用户标签集合

SADD tag:tech:user 1001 1002 1003
SMEMBERS tag:tech:user  -- 获取所有技术类用户
限流技巧:使用 SCARD 获取集合大小,结合 SADD 的返回值判断是否新增,可实现基于唯一身份数的并发限流。例如限制同时在线人数不超过 5000。

2.5 ZSet —— 滑动窗口限流标准实现

ZSet 是滑动窗口限流的最佳载体。每个请求的时间戳作为 score,成员使用唯一标识(如 UUID 或 自增 ID),通过 ZREMRANGEBYSCORE 裁剪过期窗口,ZCARD 获取当前窗口请求数。

限流应用:精确滑动窗口

-- 窗口 1 秒,最大 10 次
ZADD window:user:1001 1710000000123 "req-001"
ZREMRANGEBYSCORE window:user:1001 0 1710000000122
ZCARD window:user:1001  -- 若 > 10 则拒绝

缓存应用:排行榜与热数据排序

ZINCRBY hot_articles:2025-03 1 "article:42"
ZREVRANGE hot_articles:2025-03 0 9 WITHSCORES
内存注意:每个请求都写入 ZSet 会持续消耗内存。建议配合 EXPIRE 设置 Key 自动过期,或使用 ZREMRANGEBYRANK 定期清理最旧记录。高并发场景下可考虑本地预聚合 + 批量写入。

2.6 Hash —— 多维度计数与对象缓存

Hash 允许在同一个 Key 下管理多个字段,适合对同一用户或资源的多个维度分别计数,或缓存结构化对象的指定字段。

限流应用:多维度限流计数

HINCRBY rate:user:1001 "api:/order" 1
HINCRBY rate:user:1001 "api:/pay" 1
HGETALL rate:user:1001  -- 查看各接口调用次数

缓存应用:对象字段部分更新

HSET user:1001 name "alice" level 5 score 1200
HGET user:1001 level
HINCRBY user:1001 score 50  -- 只更新分数,不序列化整个对象
优势对比:Hash 对单个字段的读写时间复杂度 O(1),内存密度高于 String 序列化整个对象。当缓存对象字段超过 10 个且更新频繁时,Hash 比 String 节省约 30% 内存。

2.7 数据结构选型决策流程

Redis 数据结构选型决策流程 限流 / 缓存需求 需要排序 / 时间窗口裁剪? ZSet(滑动窗口限流) 需要去重? Set(黑名单 / 去重限流) 需要多字段? Hash(多维度计数 / 对象缓存) String(计数器 / 热点 KV) 说明: List 适用于 FIFO 队列场景(请求日志 / 最新消息),但滑动窗口限流优先推荐 ZSet,因其支持按 score 范围裁剪。

2.8 限流与缓存场景速查表

场景 推荐结构 关键命令 注意事项
固定窗口计数器 String INCR + EXPIRE 窗口边界突发
滑动窗口限流 ZSet ZADD + ZREMRANGEBYSCORE + ZCARD 内存持续增长,需定期清理
令牌桶 String GET + SET + Lua 需原子性补充令牌
去重限流(黑名单) Set SADD + SISMEMBER 集合过大时内存占用高
多维度接口限流 Hash HINCRBY + HGETALL 字段数不宜超过 100
热点 KV 缓存 String SET + GET 大 Value 需压缩或分片
对象部分更新缓存 Hash HSET + HGET + HINCRBY 避免频繁 HGETALL
最新 N 条消息 List LPUSH + LTRIM + LRANGE LTRIM 可能阻塞
排行榜 / 热数据排序 ZSet ZINCRBY + ZREVRANGE score 精度控制
实战建议:在限流闭环中,ZSet 滑动窗口 + Lua 脚本是精度与性能的最佳平衡点。缓存场景中,Hash 应作为对象缓存的默认选择,仅在单字段场景回退到 String。List 与 Set 在限流中更多作为辅助结构,而非核心算法载体。