在CC攻击防护中,请求频率限制是核心防线,而滑动窗口和令牌桶是两种主流算法。选择哪种,直接决定了防护的精确度、系统负载和用户体验。简单说,滑动窗口适合对实时性要求极高、需要精准控制每一秒请求量的场景;令牌桶则更适合处理突发流量、允许一定波动性的场景。如果你的业务需要严格防止任何时间点的请求超限,比如秒杀接口,就选滑动窗口;如果允许短时突发,比如API网关限流,令牌桶更灵活。下面我们拆开细节,从原理到落地,帮你一次性搞懂。

滑动窗口算法:精准到秒的流量雕刻师

滑动窗口的核心思想是把时间轴切成多个小格子(窗口),比如将1分钟切成60个1秒的格子。每个请求到来时,系统会检查当前时间所在的格子,以及向前滚动N个格子内的请求总数是否超限。这就像个移动的镜头,始终只关注最近一段时间的流量,抛弃老旧数据,从而实现精准的实时控制。例如,你设置每秒最多100请求,滑动窗口能确保任意1秒区间内都不会超过100,没有时间粒度上的“缝隙”可钻。实现上通常用环形队列或Redis的有序集合来存储时间戳,定期清理过期数据。但它的代价是计算和存储开销较大,每个请求都需要统计窗口内计数。

// 简化版滑动窗口伪代码示例
function slidingWindowLimit(userId, currentTime, limit, windowSize) {
    // 从存储中获取该用户最近windowSize秒内的请求时间戳列表
    let timestamps = getTimestamps(userId);
    // 删除早于(currentTime - windowSize)的过期记录
    while (timestamps[0] < currentTime - windowSize) {
        timestamps.shift();
    }
    // 检查当前窗口内请求数是否超限
    if (timestamps.length < limit) {
        timestamps.push(currentTime); // 记录新请求
        saveTimestamps(userId, timestamps);
        return true; // 允许通过
    }
    return false; // 拒绝请求
}

令牌桶算法:兼顾突发与平稳的流量缓冲器

令牌桶的原理是系统以一个固定速率向桶中添加令牌,桶有最大容量。每个请求需要从桶中取走一个令牌,取到则放行,桶空则拒绝。这就像个水池,一边匀速进水,一边按需放水。它最大的优势是允许突发流量:只要桶里有足够令牌,瞬间的请求高峰可以直接通过,这对于用户体验友好。例如,设置速率10令牌/秒,桶容量20,那么正常情况下每秒处理10请求,但如果有积攒,最多可以瞬间处理20个请求。令牌桶的实现通常基于时间戳和原子计数,计算开销较小。但缺点是不够“严格”,因为突发可能让短期请求量超过平均速率,对于需要绝对均匀流量的场景不适用。

// 简化版令牌桶伪代码示例
function tokenBucketLimit(userId, currentTime, rate, capacity) {
    let bucket = getBucket(userId); // 获取桶状态 {tokens, lastTime}
    let timePassed = currentTime - bucket.lastTime;
    // 计算这段时间内应添加的令牌数
    let newTokens = timePassed * rate;
    bucket.tokens = Math.min(capacity, bucket.tokens + newTokens);
    bucket.lastTime = currentTime;

    if (bucket.tokens >= 1) {
        bucket.tokens -= 1; // 消耗一个令牌
        saveBucket(userId, bucket);
        return true; // 允许通过
    }
    saveBucket(userId, bucket);
    return false; // 拒绝请求
}

关键差异对比:何时该用谁?

从精度看,滑动窗口是“硬边界”,确保任何时间窗口内不超限,适合金融交易、认证接口等零容忍场景;令牌桶是“软边界”,允许合理突发,适合内容API、下载服务等重体验场景。从性能看,滑动窗口需要维护窗口内所有请求记录,内存和CPU消耗更高;令牌桶只需维护令牌数和最后更新时间,更轻量。从实现复杂度看,滑动窗口的并发控制和数据清理更复杂;令牌桶逻辑简单,易于分布式扩展。从流量形状看,滑动窗口产出的是锯齿状均匀流量;令牌桶产出的是带突发的平滑流量。如果你的防护策略写着“严禁每秒超过N次”,就用滑动窗口;如果写着“平均速率不超过N,但可短暂超出”,就用令牌桶。

混合策略:在实际CC防护中的高级玩法

现实中,单一算法往往不够用。成熟的CC防护系统会采用多层混合策略。例如,第一层用滑动窗口做秒级精确拦截,挡住高频攻击;第二层用令牌桶做分钟级限流,允许正常用户的短暂突发;第三层结合IP、账号、行为特征做动态调整。还可以引入自适应限流,根据系统负载自动调节窗口大小或令牌速率。在分布式环境下,通常借助Redis或集群中间件实现中央限流,确保全局一致。注意,算法选择也要考虑业务类型:对电商抢购,滑动窗口为主;对视频流媒体,令牌桶更合适。永远记得测试极端场景,比如流量脉冲和持续爬虫,观察算法是否真的按预期工作。

落地注意事项与常见陷阱

首先,时间同步问题:分布式节点间必须使用同步的时间源(如NTP),否则滑动窗口的边界会错乱。第二,存储选择:滑动窗口的数据结构要支持快速范围查询和删除,Redis的zset是常用选择;令牌桶的状态存储要保证原子性,可用Redis的Lua脚本。第三,冷启动问题:令牌桶启动时桶是满的还是空的?通常设为满桶,避免服务刚启动就拒绝所有请求。第四,动态调整:限流阈值不应固定,可基于实时攻击态势自动调参。第五,用户体验:拒绝请求时返回429状态码,并考虑加入排队或延迟重试机制。避免陷阱如“误杀正常用户”——通过行为分析区分人和机器人;或“存储成为瓶颈”——对高频数据做分片和过期优化。

总结:没有最好,只有最合适

回到开头的问题,滑动窗口和令牌桶的选择,本质是在精度、性能和灵活性之间的权衡。在CC防护这场攻防战中,滑动窗口是你的精密狙击枪,一击必中;令牌桶是你的弹性护盾,化解冲击。建议你先明确防护对象的SLA和攻击模式,用小流量测试两种算法的效果,再决定核心链路用哪种。更高级的做法是混合部署,甚至自定义算法(如漏桶+滑动窗口)。记住,任何限流算法都需配合IP黑名单、人机验证、行为分析等多层防御,才能构建完整的CC防护体系。最终,让你的选择服务于业务安全与用户体验的平衡点。