2. 限流算法基础(上):固定窗口算法

固定窗口算法(Fixed Window Algorithm)是限流领域最直观、最容易实现的算法之一。它将时间划分为固定长度的窗口(例如 1 秒、1 分钟),在每个窗口内统计请求数量,一旦超过预设阈值,后续请求将被直接拒绝或排队等待。该算法在金融 API 网关、交易接口、行情推送等场景中仍有广泛应用,尤其适合对突发流量容忍度较低、且时间窗口边界精度要求不高的场景。

2.1 核心原理与数学模型

固定窗口算法的核心思想可以概括为:“窗口内计数,超限即拒绝”。其数学模型如下:

  • 窗口长度(Window Size):记为 W,单位为秒或毫秒,例如 W = 1000 ms(1 秒窗口)。
  • 阈值(Threshold):记为 T,表示每个窗口内允许的最大请求数,例如 T = 10(每秒最多 10 次请求)。
  • 当前窗口计数(Current Count):记为 C,从窗口起始时刻开始累计的请求数。
  • 判定逻辑:当新请求到达时,若 C < T,则允许请求并将 C 加 1;若 C >= T,则拒绝请求。
  • 窗口重置:当系统时间进入下一个窗口时,C 重置为 0。
关键指标: 固定窗口算法的平均限流速率 = T / W(请求/秒)。例如,W=1s,T=10,则平均速率为 10 QPS。

2.2 固定窗口算法流程图

下图展示了固定窗口算法的完整判定流程,包含窗口创建、计数检查、请求放行与拒绝、以及窗口重置四个核心阶段。

固定窗口算法流程图 固定窗口算法流程图 请求到达 窗口存在? 创建新窗口 计数 = 0 计数 < 阈值? 放行请求 计数 + 1 拒绝请求 返回 429 结束 窗口重置:时间进入下一窗口时计数归零 阈值 T:窗口内允许的最大请求数

2.3 Python 实现与代码示例

以下是一个基于 time.time() 的固定窗口限流器实现,适用于单机环境。代码包含窗口管理、计数检查、重置逻辑以及线程安全保护。

import time
import threading

class FixedWindowRateLimiter:
    """
    固定窗口限流器
    :param window_size: 窗口大小(秒)
    :param threshold: 窗口内允许的最大请求数
    """
    def __init__(self, window_size: float = 1.0, threshold: int = 10):
        self.window_size = window_size
        self.threshold = threshold
        self._lock = threading.Lock()
        self._window_start = time.time()
        self._counter = 0

    def allow_request(self) -> bool:
        """
        判断当前请求是否允许通过
        :return: True 表示放行,False 表示拒绝
        """
        with self._lock:
            now = time.time()
            # 如果当前时间已超出窗口范围,重置窗口
            if now - self._window_start >= self.window_size:
                self._window_start = now
                self._counter = 0

            # 检查计数是否超过阈值
            if self._counter < self.threshold:
                self._counter += 1
                return True
            else:
                return False

    def reset(self):
        """手动重置窗口(用于测试或特殊场景)"""
        with self._lock:
            self._window_start = time.time()
            self._counter = 0
使用建议: 在金融 API 测试中,建议将 window_size 设置为 1 秒,threshold 根据接口的 SLA 设定。例如,某行情接口承诺 100 QPS,则 threshold=100,window_size=1.0。

2.4 测试用例与运行效果

编写一个简单的测试脚本,模拟 15 个并发请求,观察限流器的行为:

def test_fixed_window():
    limiter = FixedWindowRateLimiter(window_size=1.0, threshold=10)
    allowed = 0
    denied = 0
    for i in range(15):
        if limiter.allow_request():
            allowed += 1
            print(f"请求 {i+1}: 放行")
        else:
            denied += 1
            print(f"请求 {i+1}: 拒绝 (429)")
    print(f"\n结果:放行 {allowed} 次,拒绝 {denied} 次")

if __name__ == "__main__":
    test_fixed_window()

预期输出(前 10 个请求放行,后 5 个请求被拒绝):

请求 1: 放行
请求 2: 放行
...
请求 10: 放行
请求 11: 拒绝 (429)
请求 12: 拒绝 (429)
请求 13: 拒绝 (429)
请求 14: 拒绝 (429)
请求 15: 拒绝 (429)

结果:放行 10 次,拒绝 5 次

2.5 优缺点分析

维度 说明
优点
  • 实现简单:仅需一个计数器和一个时间戳,代码量极少。
  • 性能极高:仅涉及整数加法和时间比较,无复杂计算。
  • 内存占用低:只需存储窗口起始时间和计数,适合嵌入式或边缘节点。
  • 易于理解:业务方和运维人员都能快速掌握其行为。
缺点
  • 窗口边界突发:在窗口切换瞬间可能出现两倍阈值的流量(见下方“边界效应”图示)。
  • 精度受限:窗口长度固定,无法应对毫秒级突发。
  • 不适合分布式:单机实现无法直接用于多节点限流,需配合 Redis 等中间件。
  • 缺乏平滑性:流量分布不均匀时,窗口后半段可能完全空闲,而前半段被占满。

2.6 边界效应与经典问题

固定窗口算法最著名的缺陷是“窗口边界突发”问题。假设阈值 T=10,窗口 W=1s。如果在第 1 秒的最后 100ms 内通过了 10 个请求,紧接着在第 2 秒的前 100ms 内又通过了 10 个请求,那么实际 200ms 内通过了 20 个请求,瞬时 QPS 达到 100,远超设定的 10 QPS。下图直观展示了这一现象:

固定窗口边界效应示意图 固定窗口边界效应:两倍流量突发 窗口 1(第 1 秒) 窗口 2(第 2 秒) 边界 窗口 1 末尾 100ms 内通过 10 个请求 + 窗口 2 开头 100ms 内通过 10 个请求 实际 200ms 内通过 20 个请求,瞬时 QPS = 100,远超设定阈值 10 QPS
金融场景风险: 边界突发可能导致交易接口在毫秒级窗口内承受两倍于预期的负载,引发数据库连接池耗尽或下游系统雪崩。在构建测试框架时,必须针对边界场景设计专门的压测用例,验证系统在窗口切换瞬间的稳定性。

2.7 适用场景与选型建议

  • 推荐使用:
    • 对突发流量容忍度较高的非核心接口(如日志上报、监控指标)。
    • 单机限流场景,且 QPS 阈值较低(如 < 1000)。
    • 快速原型验证或教学演示。
  • 不建议使用:
    • 金融核心交易链路(如订单、支付、风控),需要精确限流。
    • 高并发场景(QPS > 10000),窗口切换瞬间的突发可能造成系统抖动。
    • 分布式限流场景,需配合滑动窗口或令牌桶算法。