限流算法

算法 1:固定窗口

思路:把时间切成等长的「桶」(常见是按整秒),每个桶里单独计数;进入新桶时计数清零重新算。

固定窗口:按「整秒」切块计数
一次请求在系统中的判断路径
新请求到达
对齐到当前秒的窗口起点
例如整秒边界:…59、…00
读取该窗口内的已用次数
键通常包含:用户 + 窗口标识
未达上限 计数 +1,放行
已达上限 拒绝或排队
边界问题:相邻两个窗口各放行 1 次,真实间隔可以很短

规则示例:每「整秒窗口」最多 1 次请求

窗口 A 上一秒
0.9s 请求
计数未满 → 允许
窗口 B 下一秒(新窗口)
1.0s 请求
新窗口计数从零开始 → 与 A 内最后一次可紧挨着放行
从 0.9s 到 1.0s 只相隔 0.1 秒,却跨了两个窗口,各算「本秒第一次」—— 对「平滑到任意 1 秒滑动区间」来说会偏松。

小结:实现和存储都简单,但桶与桶交界处容易出现「两桶各放行一点」的突刺,对需要严格平滑的场景不够理想。

算法 2:滑动窗口

思路:不依赖对齐到整秒的边界,而是始终统计「当前时刻往前一段固定长度」内的请求数量;每次判断前丢掉这段以外的旧记录。

滑动窗口:始终看「过去固定长度」这一段时间
时间轴上的窗口在「滑动」

下图灰色条表示长度为 1 秒的观察区间,随当前时刻一起向右平移。

较早请求
当前时刻
过去
未来

落在窗口左边界之外的旧记录会被丢弃,只统计窗口内的次数。

典型实现思路(有序集合)
删掉窗口之外的旧时间戳
只保留「当前时刻 − 窗口长度」之后的记录
统计窗口内已有多少次请求
与上限比较
仍有余量 写入本次时间戳,放行
已满 限流

相比固定窗口,任意连续相同长度的时间段内,请求分布更「均匀」,边界突刺更小。

小结:在任意同样长度的连续时间区间里,流量更均匀,更接近「这段时间内最多 N 次」的直觉。

算法 3:令牌桶

思路:桶有容量上限;令牌按恒定速率进入桶(满了就停);请求到达时尝试取走一枚令牌,取到才放行。

令牌桶:按固定速率「补水」,桶满则存住突发额度
结构示意
恒定速率向桶内投放令牌(至满为止)
请求处理流程
读取桶内令牌数与上次更新时间
按经过的时间补充令牌
补充量 = 时间差 × 发放速率,且不超过桶容量
令牌 ≥ 1 消耗 1 枚,放行
令牌不足 限流或等待
突发为何被允许?
阶段 1
空闲时令牌在桶内堆满
阶段 2
短时间大量请求依次「取票」通过
阶段 3
桶空后,只能按发放速率慢慢恢复

长期平均流量仍由「发放速率」约束;短期峰值由「桶容量」吸收。

小结:长期平均速率由「投放速率」管住,短期可以吃掉桶里攒下的令牌,因此允许可控突发,很适合对外 API 与网关类场景。

算法对比

三种算法怎么选
固定窗口
实现难度
平滑度
差(边界易突刺)
突发
基本不支持
内存
常见用途
原型、极低流量、容忍粗糙
滑动窗口
实现难度
平滑度
突发
不强调突发
内存
中(需保留窗口内事件)
常见用途
需要更均匀的任意区间限流
令牌桶
实现难度
中高
平滑度
好(长期平滑)
突发
支持(受桶容量约束)
内存
低(常存少量状态)
常见用途
生产环境、API 网关常见默认思路