2. 限流算法(一):固定窗口计数器算法原理、Go实现、边界问题(突刺现象)

2.1 算法原理

固定窗口计数器(Fixed Window Counter)是最直观的限流算法。它将时间划分为固定长度的窗口(例如 1 秒),每个窗口维护一个计数器,记录该窗口内通过的请求数。当请求到达时,若当前窗口计数器未达到阈值则放行并计数加 1;若已达到阈值则直接拒绝。

核心思想: 以时间窗口为粒度,限制窗口内的最大请求总量。窗口切换时计数器重置。

算法三要素

要素 说明 典型值
窗口大小 固定时间段的长度 1s / 1min / 1h
窗口阈值 每个窗口允许的最大请求数 100 / 1000 / 10000
当前计数 窗口内已通过的请求累计值 0 → 阈值

2.2 固定窗口计数器的工作流程

下图展示了固定窗口计数器在连续两个窗口内的请求处理逻辑:

固定窗口计数器工作流程 窗口 1(0s ~ 1s)阈值=5 窗口 2(1s ~ 2s)阈值=5 请求1 请求2 请求3 计数=3/5,继续放行 请求4 请求5 请求6 计数=5/5,请求6被拒绝 时间推进 0s 1s 2s 窗口切换时计数器归零,新窗口重新计数

2.3 Go 语言实现

以下是一个线程安全的固定窗口计数器实现,使用 sync.Mutex 保护共享状态:

package limiter

import (
    "sync"
    "time"
)

// FixedWindowLimiter 固定窗口计数器限流器
type FixedWindowLimiter struct {
    mu        sync.Mutex
    limit     int           // 窗口阈值
    window    time.Duration // 窗口大小
    count     int           // 当前计数
    windowStart time.Time   // 当前窗口起始时间
}

// NewFixedWindowLimiter 创建固定窗口限流器
func NewFixedWindowLimiter(limit int, window time.Duration) *FixedWindowLimiter {
    return &FixedWindowLimiter{
        limit:       limit,
        window:      window,
        windowStart: time.Now(),
    }
}

// Allow 判断是否允许请求通过
func (l *FixedWindowLimiter) Allow() bool {
    l.mu.Lock()
    defer l.mu.Unlock()

    now := time.Now()
    // 如果已超出当前窗口,重置计数器
    if now.Sub(l.windowStart) >= l.window {
        l.count = 0
        l.windowStart = now
    }

    // 判断是否超过阈值
    if l.count < l.limit {
        l.count++
        return true
    }
    return false
}
实现要点:
  • 使用 time.Now() 判断窗口是否过期,避免依赖外部定时器。
  • 窗口重置时同时更新 windowStart 和 count,保证原子性。
  • Allow() 方法适合单次判断场景;若需批量判断,可扩展 AllowN(n int)。

2.4 边界问题:突刺现象

固定窗口计数器存在一个严重的边界缺陷——突刺现象(Traffic Spike at Window Boundary)。由于窗口切换时计数器归零,攻击者或突发流量可以在窗口边界处集中请求,导致实际 QPS 达到阈值的 2 倍。

突刺现象示例

假设阈值 = 5,窗口 = 1s:

  • 在 0.9s ~ 1.0s 内发送 5 个请求(窗口 1 允许)。
  • 在 1.0s ~ 1.1s 内发送 5 个请求(窗口 2 允许)。
  • 实际 0.2s 内通过了 10 个请求,瞬时 QPS = 50,远超阈值 5。
突刺现象的本质: 固定窗口只限制窗口内的总量,不限制窗口边界处的请求密度。窗口切换的瞬间,计数器重置为 0,导致边界两侧的请求可以“接力”通过。

2.5 突刺现象的视觉化分析

下图展示了突刺现象如何让瞬时流量突破阈值:

突刺现象:窗口边界处的流量尖峰 窗口 1(0s ~ 1s) 窗口 2(1s ~ 2s) 请求A 请求B 请求C 请求D E F 请求G 请求H 请求I 请求J 0.0s 1.0s 2.0s 突刺区域 0.2s 内通过 10 个请求,瞬时 QPS = 50,是阈值 5 的 10 倍 窗口边界处计数器重置,导致流量“接力”通过

2.6 突刺现象的量化分析

指标 正常情况 突刺情况 恶化倍数
窗口阈值 5 req/s 5 req/s —
实际通过量 5 req/s 10 req / 0.2s 10×
瞬时 QPS 5 50 10×
后端压力 可控 可能击穿 —

2.7 突刺现象的应对思路

突刺现象是固定窗口计数器的固有缺陷,无法通过参数调优彻底消除。常见的应对策略包括:

  • 缩小窗口粒度: 将 1s 窗口拆分为 100ms 窗口,降低单窗口容量,但无法根除边界问题。
  • 引入滑动窗口: 将多个子窗口的计数叠加,平滑边界效应(下一章详细讲解)。
  • 配合漏桶/令牌桶: 在固定窗口基础上增加速率限制,控制请求的到达间隔。
  • 增加前置缓冲: 在限流器前加一层请求队列,削峰填谷。
工程建议: 固定窗口计数器适合对精度要求不高的场景(如小时级/天级配额限制)。对于秒级限流,尤其是需要防御突发流量的场景,建议直接使用滑动窗口或令牌桶算法。

2.8 本章小结

  • 固定窗口计数器以固定时间窗口为单位,限制窗口内的请求总量,实现简单、性能高。
  • Go 实现使用 sync.Mutex 保护计数器和窗口起始时间,窗口过期时重置。
  • 突刺现象是固定窗口的核心缺陷:窗口边界处计数器重置,导致瞬时流量可达阈值的 2 倍甚至更高。
  • 突刺现象无法通过参数调优消除,需要引入滑动窗口或令牌桶等更平滑的算法。