滑动窗口限流在统计时间窗口上怎么实现
摘要:# 滑动窗口限流:别被“时间窗口”四个字吓到,它其实挺接地气的 我前两天帮一个做电商的朋友看后台,好家伙,凌晨一波促销活动,接口直接被打挂了。查日志一看,全是同一个IP在疯狂刷券。他一脸懵地问我:“我明明设了每分钟100次的限流啊,怎么没防住?” 我一…
滑动窗口限流:别被“时间窗口”四个字吓到,它其实挺接地气的
我前两天帮一个做电商的朋友看后台,好家伙,凌晨一波促销活动,接口直接被打挂了。查日志一看,全是同一个IP在疯狂刷券。他一脸懵地问我:“我明明设了每分钟100次的限流啊,怎么没防住?”
我一看他那配置就乐了:“老哥,你用的是固定窗口吧?人家卡着59秒那一下狂发请求,你这两分钟交界处,可不就是‘法外之地’嘛。”
他恍然大悟的表情,我见得太多了。很多刚接触限流的朋友,都会被“滑动窗口”这个词唬住,觉得是什么高深算法。说白了,它就是个更“较真”、更“抠细节”的计数器。
今天,咱就抛开那些让人头大的数学公式和论文术语,像唠家常一样,把“滑动窗口限流”到底怎么在统计时间窗口上实现,给你掰扯明白。
一、固定窗口的“致命漏洞”:时间边界上的“偷渡客”
在讲滑动窗口之前,你得先知道它为啥被发明出来。
想象一下,你开了一家小面馆,规定每个小时只接待100位客人(这就是固定窗口限流,窗口长度1小时)。
- 第一种情况:59分59秒的时候,一下子涌进来50个人。你的计数器显示,这个小时才接待了60个人,没超限,全放进来。没问题。
- 第二种情况(漏洞来了):59分59秒,又涌进来50个人。你放行了,计数器变成110。但下一秒,时钟跳到整点,计数器清零了!0分01秒,又来了50个人。新窗口刚开始,你又放行了。
发现问题了吗?在59分59秒到0分01秒这短短两三秒里,实际进来了100个人,但你两个窗口的计数都没超标。你的店在时间边界上被瞬间挤爆了。
这种攻击,在技术上叫“窗口边界攻击”。很多所谓的CC攻击(挑战黑洞攻击),就爱这么玩。固定窗口限流,防君子不防小人,更防不住有备而来的攻击者。
二、滑动窗口:一个“斤斤计较”的移动哨兵
那滑动窗口怎么解决这个问题呢?它不搞“整点清零”那一套,而是变得特别“斤斤计较”。
它还规定每小时100人,但它手里拿的不是一个小时的完整名单,而是一个不断向前滑动的、长度为1小时的名单。
举个例子你就懂了:
现在是下午2点30分。滑动窗口关心的是从1点30分到2点30分这一个小时里,来了多少人。而不是机械地看“2点这个小时”来了多少人。
- 假设1点31分来了10个人。
- 1点45分来了30个人。
- 2点10分来了40个人。
- 2点28分来了15个人。
那么,在2点30分这一刻,滑动窗口统计的范围是1:30-2:30。它会把1点31分(在范围内)的10个人算上,但1点30分整来的客人(如果有的化),因为刚好超出当前窗口的起点,就不算了。
当时间来到2点31分,这个窗口就向前滑动1分钟,变成统计1点31分到2点31分这个区间的人数。刚才1点31分来的那10个人,因为刚好滑出窗口,就不计入统计了。
看出精髓了吗? 滑动窗口在任何一个时间点的统计,都是看刚刚过去的、完整的一个窗口期内的流量。它没有固定的“清零时刻”,攻击者再也找不到那个可以“偷渡”的时间边界了。
三、具体怎么实现?两种接地气的思路
理论有点绕,咱们上点干货。在实际敲代码或者配置的时候,怎么搞出这个滑动窗口呢?常见的有两种接地气的办法。
思路一:桶分片法(把大窗口切成小格子)
这是最直观、对程序员最友好的一种思路。你不是统计一个小时吗?我嫌一个小时太“糙”了,我把这一个小时切成60个“小格子”,每个格子管1分钟。
- 怎么存? 我搞一个数组或者链表,长度就是60,每个位置代表一分钟内的请求数。
- 怎么滑? 当前时间是第几分钟,我就更新对应格子的计数。同时,我永远只统计最近60个格子的总和。
- 怎么判断超限? 每次请求进来,我就把最近60个格子的数字加起来,看看超没超过100。没超过,放行,并把当前1分钟格子的计数+1;超过了,就拒绝。
好处是啥? 计算简单,内存固定。你不需要真的保存过去一小时每一个请求的时间戳(那得海了去了),你只需要维护60个计数器。精度虽然损失了一点点(以1分钟为最小单位),但对付绝大多数场景,绰绰有余。
很多开源的限流组件,像Redis的redis-cell模块(用了GCRA算法,本质是高级滑动窗口),或者你在Go里用github.com/juju/ratelimit,底层思想都和这个类似。
思路二:时间戳队列法(老实人的记账本)
如果你追求极限精确,不嫌麻烦,可以用这个“老实人”办法。
- 怎么存? 我维护一个队列(比如Redis的List或Sorted Set),每次请求进来,就把它的时间戳塞到队列里。
- 怎么滑? 每次有新请求进来要判断时,我就从队列里,把那些时间戳小于(当前时间 - 1小时)的旧记录全部踢出去。因为这些请求已经不在我关心的滑动窗口内了。
- 怎么判断超限? 踢掉旧记录后,看看队列里还剩多少条记录(这就是过去一小时内的请求数)。如果小于100,就把当前时间戳塞进去,放行;否则,拒绝。
听起来很完美对吧?但有个坑: 如果流量很大,这个队列可能会非常长,频繁地插入和删除操作,对内存和CPU都是考验。所以它通常适用于QPS(每秒请求数)不是特别夸张的场景。
我自己的经验是,95%的情况下,用桶分片法就足够了。精度和性能平衡得最好。除非你做的是金融交易风控,对时间敏感度要到毫秒级,那才值得去折腾时间戳队列。
四、说点大实话:滑动窗口不是银弹
看到这里,你可能觉得滑动窗口简直完美。别急,我得给你泼点冷水,这也是很多方案里不会明说的。
- 它“防不住”海量分布式攻击。 滑动窗口解决了时间边界问题,但如果攻击者用十万个不同的IP,每个IP都慢悠悠地、均匀地发送请求,让你的总请求数刚好卡在阈值附近,你的服务照样会被拖慢。这时就需要结合IP限流、用户ID限流、甚至行为指纹等多维策略了。光靠一个技术点就想高枕无忧?想多了。
- 它不能“削峰填谷”。 滑动窗口只负责坚决地拒绝超限的请求。如果正常的业务洪峰来了(比如明星官宣结婚,你的App是独家合作平台),窗口期内的请求就是会超限,它就会无情地拒绝掉一部分用户。这时候,你需要的是排队队列、服务降级、弹性扩容这些更上层的手段。限流是保护系统不垮,不是保证用户体验完美。
- 配置需要“手感”。 窗口长度设多长?1分钟?5分钟?10分钟?限流阈值设多少?1000?5000?这玩意没有标准答案。设得太紧,正常用户动不动被拒;设得太松,攻击来了又防不住。这得靠监控数据慢慢调,是个经验活。 我一般建议,先根据压测和平时峰值的2-3倍来设,上线后盯着告警和日志再微调。
写在最后:技术是手段,不是目的
聊了这么多,其实我想说的就一点:滑动窗口限流,是一个比固定窗口“聪明”一点的计数器算法。 它的核心价值,就是堵上了固定窗口那个愚蠢的时间漏洞,让限流变得更精准、更公平。
但你也别把它神化。在真正的系统防护里,它通常只是WAF(Web应用防火墙)或网关里一个默默无闻的模块,是多层防御体系中的一环。它的上面有防火墙扛大流量,旁边有验证码防机器人,下面有业务代码本身的优化。
所以,下次当你再听到“滑动窗口限流”时,脑子里就浮现出那个“斤斤计较”、不断向前移动的哨兵就行了。它很重要,但也不必为它感到焦虑。
真正的安全,从来不是靠一个炫酷的算法实现的,而是靠一整套严谨的、有层次的、经过实战考验的架构和运维习惯。
行了,关于“时间窗口”那点事,今天就唠到这儿。如果你的系统还在用固定窗口,是时候考虑“滑动”一下了。

