用Redis做滑动窗口那事儿,怎么实现才靠谱又高效呢
- 问答
- 2026-01-24 03:41:40
- 2
行,那咱们就直接聊怎么用Redis搞一个既靠谱又高效的滑动窗口限流,这事儿说白了,就像你家门口有个旋转门,规定一分钟内只能进10个人,你得实时盯着,看过去60秒里到底进了几个,超了就得拦着,用Redis来做,核心就是利用它的数据结构和原子操作,把“过去某段时间”的计数精准又快速地算出来。

核心思想:把时间切成小格子
最靠谱、最常用的法子,就是把时间窗口划分成一个个更小的时间片(1分钟的窗口,切成6个10秒的小格子),这么做有个天大的好处:不用记录每个请求的精确时间戳,那样存储和计算成本都太高,想象一下,如果每秒来几百个请求,每个请求都存一个时间戳,Redis的内存很快就爆了,清理旧数据也是个噩梦。

来源参考:这种基于时间片的滑动窗口算法,在业界常被称为“Sliding Window Log”的优化版本,或者直接叫“Redis滑动窗口计数”,是平衡精度和性能的经典实践。

具体咋搞呢?
- 确定窗口和格子大小:限流规则是“每分钟最多100次请求”,我们可以把1分钟(60秒)的窗口划分为60个格子,每个格子代表1秒,这样,我们实际记录的是最近60秒内,每一秒发生的请求数。
- 用Redis的Sorted Set来存:这是实现的关键,我们用Sorted Set来存这些“小格子”,这个Sorted Set的
key可以叫rate_limit:user123(假设按用户限流),Set里的每个member是一个唯一标识(比如用UUID或者毫秒时间戳+随机数,确保不重复),而它的score非常重要,要设置为这个请求所属的那个时间片的起始时间戳。- 举个例子:现在是北京时间14:05:23.500秒,我们按秒切分,那么这个请求的
score14:05:23对应的整秒时间戳(1621231523)。
- 举个例子:现在是北京时间14:05:23.500秒,我们按秒切分,那么这个请求的
- 来了一个请求,干嘛?
- 第一步:获取当前时间戳,记作
now。 - 第二步:计算窗口的起始时间,比如窗口是60秒,那么起始时间
window_startnow - 59(因为是包含当前秒的60秒),注意,这里减59是为了得到60个完整的秒级格子。 - 第三步:清理旧格子,立刻使用
ZREMRANGEBYSCORE命令,把score小于window_start的所有member都删掉,这一步是灵魂!它保证了Sorted Set里永远只保存最近60秒的请求记录,自动实现了窗口的滑动,而且内存不会无限增长。- 命令长这样:
ZREMRANGEBYSCORE rate_limit:user123 0 (window_start
- 命令长这样:
- 第四步:统计当前窗口内的请求总数,用
ZCARD命令(获取Sorted Set的成员数量)来统计,这个操作是O(1)复杂度,飞快。 - 第五步:判断是否超限,如果总数小于100,允许通过;否则,就拒绝。
- 第六步:记录当前请求,如果允许通过,那么就用
ZADD命令,把当前请求的标识(member)和它所属时间片的score(比如now的整秒时间戳)加到Sorted Set里,给这个Sorted Set设置一个过期时间(TTL),比如61秒。设置TTL是必须的,这是个安全网,万一后续一段时间没请求,没人触发第三步的清理,这个Key也能自动过期删除,避免垃圾数据堆积。
- 第一步:获取当前时间戳,记作
为啥说这个方法靠谱又高效?
- 精准:相比于更简单的固定窗口算法(比如每分钟重置一次计数器,在窗口切换的瞬间可能会有两倍流量涌入),滑动窗口能精确地统计任意时刻开始往前推60秒内的请求量,限流更平滑、更准确。
- 高效:核心操作就俩:
ZREMRANGEBYSCORE(清理)和ZCARD(计数),在Redis里,这些操作都非常快,通过清理旧数据,整个存储空间是固定的,不会因为运行时间长而变慢或内存溢出。 - 原子性(关键!):上面的第三步到第六步,必须保证是原子操作,不然,在并发请求时,可能会出现判断没超限,但多个请求同时添加后实际超限的“误判”情况,怎么保证原子性?有两种主流选择:
- 使用Redis Lua脚本:这是最推荐的方式,你可以把清理、计数、判断、添加、设置TTL等一系列操作写在一个Lua脚本里,Redis会单线程执行这个脚本,确保在执行过程中不会有其他命令插队,完美解决并发问题。
- 使用管道(pipeline)配合WATCH:稍微复杂一点,用
WATCH命令监听这个限流Key,然后执行一系列操作,最后用MULTI/EXEC事务提交,如果期间Key被其他客户端修改了,事务会失败,需要重试,这种方式不如Lua脚本简洁和高效。
还能不能再优化一下?
当然可以,对于流量特别大、精度要求不是那么极致的场景,可以进一步优化:
- 减少时间片数量:1分钟的窗口,不一定要切成60个1秒的格子,可以切成10个6秒的格子,这样,Sorted Set里的成员数量最大只有10个,
ZCARD和清理操作更快,存储也更小,代价是精度下降,比如你只能统计到“过去~54秒到~60秒”的请求被算在了同一个格子里。 - 使用Hash结构(近似计数):如果你能接受一点点误差,可以用Hash,Key是限流标识,Field是时间片索引(
timestamp / interval的整数值),Value是这个时间片内的计数,每次请求就对当前时间片Field的值做HINCRBY,统计时,用HGETALL取出所有Field,把在时间窗口内的Field值加起来,同时用HDEL删除旧的Field,这个方案在时间片较大时,命令数可能更少,但统计时需要客户端做简单计算,并且不是绝对精确。
总结一下
用Redis做滑动窗口限流,最“靠谱又高效”的黄金组合是:Sorted Set + 时间分片 + Lua脚本原子化操作。
- Sorted Set 负责精准地按时间范围存储和清理请求。
- 时间分片 是平衡性能和精度的关键技巧,避免了记录每个时间点的开销。
- Lua脚本 确保了在高并发下,判断和计数不会出现竞态条件,结果是准确的。
把这三点做到位,实现一个能扛住生产环境流量冲击的滑动窗口限流器,基本就没啥大问题了,最后一定要加上Key的过期时间,这是良好的习惯。
本文由芮以莲于2026-01-24发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://www.haoid.cn/wenda/84851.html
