滑动窗口算法是对“固定窗口算法”的一种优化,旨在解决窗口边界处的流量翻倍问题。

背景:固定窗口的缺陷

如果设置限流为“每秒 100 个请求”,使用固定窗口:

  • 0.9s 时涌入 100 个请求。
  • 1.1s 时又涌入 100 个请求。
  • 站在固定窗口([0,1] 和 [1,2])的角度,都没有超速。但在 [0.9, 1.1] 这短短 0.2s 内,系统处理了 200 个请求,瞬间压力翻倍。

滑动窗口原理

逻辑图解

时间轴 (1秒窗口)
|-----|-----|-----|-----|-----|
  q1    q2    q3    q4    q5   (每个格子 200ms)
 
[       当前 1s 统计窗口        ]
随着时间推移,窗口向右滑动一格:
      |-----|-----|-----|-----|-----|
        q2    q3    q4    q5    q6
      [        新的 1s 统计窗口       ]
  1. 时间分片:将 1 秒的时间窗口划分为多个小格子(如 5 个格子,每个格子 200ms)。
  2. 窗口滑动:随着时间推移,整个窗口向前滑动,每次滑动一个格子的宽度。
  3. 动态计数:统计当前窗口内所有格子的请求总数。

Go 语言实现 (示例)

type SlidingWindow struct {
    limit      int           // 限流阈值
    windowSize time.Duration // 窗口大小
    slotCount  int           // 格子数量
    slots      []int         // 每个格子的计数
    slotDur    time.Duration // 每个格子的时长
    mu         sync.Mutex
}
 
func (w *SlidingWindow) Allow() bool {
    w.mu.Lock()
    defer w.mu.Unlock()
 
    now := time.Now()
    // 1. 计算当前时间落在哪一格 
    // (此处简化了滑动逻辑,实际生产中需使用循环数组并清理过期槽位)
    currentIdx := (now.UnixNano() / int64(w.slotDur)) % int64(w.slotCount)
    
    // 2. 统计当前窗口内所有格子的请求总数
    sum := 0
    for _, count := range w.slots {
        sum += count
    }
 
    if sum < w.limit {
        w.slots[currentIdx]++
        return true
    }
    return false
}

核心特点

  • 精度高:格子划分得越细,滑动窗口的限流就越平滑。
  • 解决临界问题:由于统计的是一个动态的时间区间,成功规避了固定窗口在边界处流量翻倍的风险。

应用场景

  • Sentinel:阿里巴巴的开源限流组件 Sentinel 核心就采用了滑动窗口(LeapArray)。
  • TCP 流量控制:TCP 协议中著名的滑动窗口也是基于类似的滑动窗口思想。