CC防护中的频率限制,核心就是两套算法在打擂台:滑动窗口和漏桶。滑动窗口更适合应对突发流量尖峰,漏桶则擅长平滑处理持续高频请求。实际场景里,大多数成熟的WAF和CDN防护系统并不是二选一,而是把两者结合起来用——滑动窗口做粗粒度的请求计数,漏桶做细粒度的流量整形。如果你正在搭建防护策略,我直接给结论:短时间高并发攻击用滑动窗口,长时间持续刷请求用漏桶,混合场景就叠加使用。下面我把这两种算法的原理、实现方式、优缺点和真实场景对比全部拆开讲清楚。
一、滑动窗口算法:怎么算的、怎么实现的
滑动窗口的核心思想很简单:设定一个时间窗口(比如60秒),统计这个窗口内的请求次数,超过阈值就拒绝。但这里有个关键问题——固定窗口会有边界效应。比如你设60秒窗口、阈值100次,攻击者在第59秒发50次、第61秒又发50次,两个窗口各没超限,但实际上1秒内就打了100次。滑动窗口就是为了解决这个问题,它把大窗口切成多个小格子,每次滑动时丢掉最旧的格子、加入最新的格子,实时计算总和。
具体实现上,最常用的是基于Redis的方案。用Sorted Set存储每个请求的时间戳,每次请求进来时先清除过期数据,再统计剩余数量。代码逻辑如下:
// 滑动窗口限流 - 基于Redis实现
function slidingWindowLimit(userId, maxRequests, windowSeconds) {
const key = `cc_limit:${userId}`;
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
// 清除过期时间戳
redis.zremrangebyscore(key, 0, windowStart);
// 统计当前窗口内请求数
const currentCount = redis.zcard(key);
if (currentCount >= maxRequests) {
return { allowed: false, reason: 'frequency_exceeded' };
}
// 记录本次请求
redis.zadd(key, now, `${now}_${Math.random()}`);
redis.expire(key, windowSeconds + 10);
return { allowed: true, count: currentCount + 1 };
}
滑动窗口的优势在于精度高、边界问题少,而且天然支持多维度统计——按IP、按用户ID、按接口路径都能做。但它的缺点也明显:内存消耗大,每个请求都要存时间戳;在极高QPS下,Redis的ZREM和ZCARD操作会成为瓶颈;另外它只管计数不管速率,无法对流量做平滑处理。
二、漏桶算法:怎么算的、怎么实现的
漏桶的模型更直观:想象一个底部有孔的桶,水(请求)从上面倒进去,桶以固定速率从下面漏出去。如果倒水速度超过漏水速度,桶满了就溢出——多余的请求直接丢弃。漏桶的特点是输出速率恒定,不管输入多猛,出去的流量永远是平滑的。
实现上,漏桶通常用令牌桶的变体或者直接用计数器+定时泄漏来做。下面是一个简化的漏桶实现:
// 漏桶限流 - 内存实现(单机版)
class LeakyBucket {
constructor(capacity, leakRate) {
this.capacity = capacity; // 桶容量
this.leakRate = leakRate; // 每秒漏出量
this.tokens = 0; // 当前令牌数
this.lastLeakTime = Date.now();
}
allowRequest() {
const now = Date.now();
// 计算泄漏的令牌数
const elapsed = (now - this.lastLeakTime) / 1000;
this.tokens = Math.max(0, this.tokens - elapsed * this.leakRate);
this.lastLeakTime = now;
if (this.tokens < this.capacity) {
this.tokens += 1;
return true;
}
return false;
}
}
// 使用示例:容量100,每秒漏10个
const bucket = new LeakyBucket(100, 10);
漏桶的优点是输出流量绝对平稳,特别适合保护后端数据库、API接口这类对突发流量敏感的资源。但它的硬伤是无法应对合法的突发流量——比如秒杀场景,用户短时间集中访问是正常的,漏桶会把这些请求全部压平甚至丢弃,造成误杀。而且单机版漏桶在分布式环境下需要额外同步机制,否则多节点各自计数会导致限流失效。
三、实际场景对比:什么时候用哪个
我把实际业务场景分成几类,逐一对比两种算法的表现:
场景1:API接口防刷
比如你有一个登录接口,正常用户每分钟请求不超过30次,但攻击者用脚本每秒打200次。这种场景滑动窗口更合适。设60秒窗口、阈值30次,攻击者的高频请求会迅速触发限制。漏桶在这里反而不好——如果你设每秒漏5个,正常用户连续输错密码重试也会被卡住。
场景2:下载接口保护
文件下载接口如果不限速,一个用户可以把带宽吃光。漏桶在这里是最佳选择,设定每秒最多10MB流出,不管用户怎么请求,服务器输出永远稳定。滑动窗口只管次数不管带宽,对这种场景无能为力。
场景3:短信验证码防轰炸
短信接口既要防高频又要防突发。最佳实践是滑动窗口+漏桶叠加:滑动窗口统计每个手机号5分钟内不超过5次,漏桶控制整体短信发送速率不超过每秒100条。这样既防单用户刷,又防全局压垮短信通道。
场景4:电商秒杀/抢购
这是最棘手的场景。合法用户会在开抢瞬间涌入,滑动窗口可以允许短时间内的高并发(比如1秒内500次算正常),而漏桶会把流量压平导致用户抢不到。所以秒杀场景通常用滑动窗口做前置过滤,后端再用队列削峰,漏桶基本不用。
四、性能和工程实现的硬核对比
从性能角度看,滑动窗口依赖存储操作,每次请求至少一次ZREM+一次ZCARD+一次ZADD,在Redis集群下单次操作约0.5-1ms,高QPS时网络往返是主要开销。漏桶如果用内存实现,单次请求就是几次算术运算,纳秒级;但分布式漏桶需要用Redis的INCR+EXPIRE或者Lua脚本保证原子性,性能会下降到和滑动窗口同一量级。
从精度角度看,滑动窗口可以做到秒级甚至毫秒级的精确计数,漏桶的精度取决于泄漏计算的频率——如果每100ms计算一次泄漏,精度就是100ms级别。对于CC防护来说,秒级精度通常够用,但面对每秒数万次的攻击,毫秒级精度更有优势。
从可扩展性看,滑动窗口天然支持多维度——IP维度、用户维度、接口维度可以并行统计,互不干扰。漏桶如果要多维度就需要多个桶实例,内存占用线性增长。在大型系统中,滑动窗口的多维度能力是明显优势。
五、行业主流做法和我的建议
目前主流的CC防护方案,无论是商业WAF还是自研系统,基本都是混合架构:第一层用滑动窗口做快速计数和粗粒度拦截,第二层用漏桶或令牌桶做流量整形和细粒度控制,第三层用行为分析和机器学习做智能识别。单纯依赖一种算法的系统,在复杂攻击面前都会有盲区。
我的实操建议是:如果你资源有限只能选一种,优先选滑动窗口,因为它覆盖面更广、适应性更强。如果你的后端资源特别脆弱(比如老旧数据库),一定要加上漏桶做保护。另外,别忘了设置合理的阈值——滑动窗口的窗口大小和阈值需要根据业务基线来定,建议先跑一周正常流量数据,取P95或P99值的1.5-2倍作为阈值,既不误杀也不放过。
还有一点很多人忽略:CC防护不只是算法问题,还有策略组合问题。比如针对同一个IP,可以同时跑滑动窗口计数和漏桶整形,两个条件都满足才放行,这种"与"逻辑的组合策略比单一算法强得多。同时要做好白名单机制,把搜索引擎爬虫、监控探针、已知合作方IP排除在外,避免防护策略误伤正常流量。
六、总结
滑动窗口和漏桶不是对立关系,而是互补关系。滑动窗口解决"多少次"的问题,漏桶解决"多快"的问题。CC防护的本质是在安全和可用性之间找平衡,算法只是工具,真正的功夫在于对业务场景的理解和策略的精细调优。把两种算法吃透、灵活组合,你的防护体系才能真正扛住实战考验。
