3. 滑动窗口算法:原理详解、Python实现与精度优化

固定窗口算法在窗口切换瞬间存在流量突刺的漏洞,而滑动窗口算法通过将时间窗口细分为多个更小的“子窗口”(Slot),以更精细的粒度统计请求量,从而平滑流量控制,是金融级限流场景中的核心算法之一。

3.1 滑动窗口核心原理

滑动窗口不再使用单一的全局计数器,而是将一个完整的时间窗口(例如 1 秒)划分为 N 个等长的子窗口。每个子窗口维护独立的计数器。当请求到达时,算法计算当前时间点所属的窗口范围(包含当前子窗口及之前的 N-1 个子窗口),累加这些子窗口的计数,若总和超过阈值则触发限流。

关键概念对比:固定窗口 vs 滑动窗口
对比维度 固定窗口 滑动窗口
统计粒度 整个窗口一个计数器 窗口内多个子窗口计数器
边界突刺 窗口切换时允许 2 倍阈值流量 平滑过渡,无突刺
内存占用 O(1) O(N),N 为子窗口数量
精度控制 粗粒度 细粒度,精度由 N 决定
适用场景 非关键路径、允许毛刺 金融交易、API 网关核心限流
滑动窗口算法原理图 滑动窗口算法原理(1秒窗口,4个子窗口) Slot 1 [0, 250ms] Slot 2 [250, 500ms] Slot 3 [500, 750ms] Slot 4 [750, 1000ms] 当前滑动窗口(覆盖 Slot 2 + Slot 3 + Slot 4) 当前时间 t=800ms 说明:当请求在 t=800ms 到达时,算法统计 Slot 2、Slot 3、Slot 4 的计数之和。 Slot 1 已过期被丢弃,窗口始终向前滑动,保证统计区间为最近的 1 秒。

3.2 Python 基础实现

以下实现使用 collections.deque 存储子窗口记录,每个元素为 (timestamp, count) 元组。核心逻辑包括:清理过期子窗口、累加有效计数、判断是否限流。

import time
from collections import deque

class SlidingWindowLimiter:
    """滑动窗口限流器(基础版)"""
    def __init__(self, max_requests: int, window_size: float = 1.0, slots: int = 10):
        """
        :param max_requests: 窗口内允许的最大请求数
        :param window_size: 窗口大小(秒)
        :param slots: 子窗口数量(精度控制)
        """
        self.max_requests = max_requests
        self.window_size = window_size
        self.slot_duration = window_size / slots  # 每个子窗口时长
        self.slots = deque()  # 存储 (slot_start_time, count)

    def _get_current_slot(self) -> float:
        """计算当前时间所属的子窗口起始时间"""
        now = time.time()
        return now - (now % self.slot_duration)

    def _clean_expired_slots(self):
        """移除所有已过期的子窗口"""
        now = time.time()
        window_start = now - self.window_size
        while self.slots and self.slots[0][0] < window_start:
            self.slots.popleft()

    def allow_request(self) -> bool:
        """判断是否允许当前请求"""
        self._clean_expired_slots()
        current_slot_start = self._get_current_slot()

        # 更新或追加当前子窗口
        if self.slots and self.slots[-1][0] == current_slot_start:
            # 当前子窗口已存在,增加计数
            self.slots[-1] = (current_slot_start, self.slots[-1][1] + 1)
        else:
            # 创建新的子窗口
            self.slots.append((current_slot_start, 1))

        # 计算当前滑动窗口内总请求数
        total_requests = sum(count for _, count in self.slots)
        return total_requests <= self.max_requests
实现要点:
  • 子窗口对齐:使用 now - (now % slot_duration) 将时间戳对齐到子窗口边界,避免浮点误差导致窗口错位。
  • 过期清理:每次请求前清理 window_start 之前的子窗口,保证 deque 长度不超过 slots + 1。
  • 计数更新:当前子窗口存在则累加,不存在则追加,避免重复创建。

3.3 精度优化:子窗口数量与内存权衡

滑动窗口的精度由子窗口数量 slots 决定。slots 越大,统计粒度越细,但内存占用和计算开销也线性增长。金融场景通常需要在精度与性能之间找到平衡点。

3.3.1 精度与性能对比

子窗口数 (slots) 理论最大误差 内存占用 (deque) 推荐场景
5 20% 窗口大小 ~5 个元素 非关键路径、日志采样
10 10% 窗口大小 ~10 个元素 通用 API 限流
20 5% 窗口大小 ~20 个元素 金融交易、高精度控制
50 2% 窗口大小 ~50 个元素 高频交易、毫秒级限流

3.3.2 优化策略:动态子窗口合并

在高并发场景下,固定 slots 可能导致频繁的 deque 操作。优化思路是:当子窗口内无请求时,跳过创建;当连续多个子窗口计数为 0 时,合并为一个过期标记,减少元素数量。

class OptimizedSlidingWindowLimiter(SlidingWindowLimiter):
    """优化版:跳过空子窗口,减少内存碎片"""
    def allow_request(self) -> bool:
        self._clean_expired_slots()
        current_slot_start = self._get_current_slot()

        # 优化:如果当前子窗口与最后一个子窗口不连续,插入空窗口标记
        if self.slots and self.slots[-1][0] < current_slot_start - self.slot_duration:
            # 存在时间间隙,插入一个空窗口(仅用于占位,不增加计数)
            self.slots.append((current_slot_start - self.slot_duration, 0))

        # 更新或追加当前子窗口
        if self.slots and self.slots[-1][0] == current_slot_start:
            self.slots[-1] = (current_slot_start, self.slots[-1][1] + 1)
        else:
            self.slots.append((current_slot_start, 1))

        total_requests = sum(count for _, count in self.slots)
        return total_requests <= self.max_requests
精度优化注意事项:
  • 时间精度:Python 的 time.time() 精度受操作系统影响,Windows 下约 15ms,Linux 下约 1μs。金融场景建议使用 time.perf_counter() 或 time.monotonic()。
  • 内存回收:长时间无请求时,deque 可能保留过期空窗口。可增加定时清理或惰性清理策略。
  • 原子操作:多线程/多进程环境下,deque 操作非线程安全。需加锁或使用 Redis 的 Sorted Set 实现分布式滑动窗口。
滑动窗口精度优化对比 精度优化对比:子窗口数量对误差的影响 低精度:5 个子窗口 S1 S2 S3 S4 S5 每个子窗口 = 200ms 最大误差:±200ms(20%) 内存占用:~5 元素 高精度:20 个子窗口 每个子窗口 = 50ms 最大误差:±50ms(5%) 内存占用:~20 元素 权衡:slots 每增加 1 倍,误差减半,但内存和计算开销线性增长。金融场景推荐 10~20 个子窗口。

3.4 实战案例:金融 API 限流配置

假设某金融数据 API 的限流规则为:每秒最多 100 次请求,要求误差不超过 10%。根据精度要求,选择 slots=10,每个子窗口 100ms,理论最大误差 10%。

# 实战配置示例
limiter = SlidingWindowLimiter(
    max_requests=100,
    window_size=1.0,
    slots=10  # 每个子窗口 100ms,误差 ≤ 10%
)

# 模拟请求
for i in range(120):
    if limiter.allow_request():
        print(f"请求 {i+1}: 允许")
    else:
        print(f"请求 {i+1}: 限流(拒绝)")
    time.sleep(0.005)  # 模拟 5ms 间隔
输出结果分析:
  • 前 100 个请求全部通过。
  • 第 101~120 个请求中,部分被限流,具体取决于请求在子窗口内的分布。
  • 由于滑动窗口的平滑特性,不会出现固定窗口的“双倍突刺”现象。

3.5 滑动窗口 vs 其他算法选型建议

算法 精度 内存 实现复杂度 金融推荐度
固定窗口 低 低 极低 ★☆☆☆☆
滑动窗口 中-高 中 中 ★★★★☆
令牌桶 高 低 中 ★★★★★
漏桶 高 低 低-中 ★★★☆☆