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 数据结构选型决策流程
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 在限流中更多作为辅助结构,而非核心算法载体。