4. 滑动窗口算法:滑动日志与滑动窗口原理、精度与内存权衡、在秒杀/抢购场景的应用
滑动窗口算法是金融级限流中最常用的时间窗口算法,它解决了固定窗口在边界突发流量下的“窗口跳跃”缺陷。本章深入剖析滑动日志与滑动窗口两种实现方式,对比精度与内存开销,并给出秒杀/抢购场景下的最佳实践。
4.1 固定窗口的边界缺陷
固定窗口算法(如每秒100次、每分钟6000次)在窗口切换瞬间存在严重漏洞:
- 窗口跳跃:若请求集中在窗口末尾和下一个窗口开头,实际瞬时QPS可达阈值的2倍。
- 不公平抢占:恶意客户端可精准在窗口边界发起洪峰,绕过限流。
- 统计失真:金融风控要求平滑流量,固定窗口无法满足。
金融合规要求:银保监会《网上银行系统信息安全通用规范》明确要求“对交易接口实施平滑限流,防止窗口边界冲击”。滑动窗口是满足该要求的推荐方案。
4.2 滑动日志算法(精确但昂贵)
滑动日志为每个请求记录时间戳,每次判断时统计当前时间往前推一个窗口长度内的请求总数。本质上是时间戳集合的实时查询。
4.2.1 核心原理
- 维护一个有序的时间戳列表(通常用Redis ZSET或内存跳表)。
- 新请求到达时,将当前时间戳加入列表。
- 移除窗口范围之外(
now - windowSize)的过期时间戳。 - 统计剩余时间戳数量,若超过阈值则拒绝。
4.2.2 精度与内存权衡
| 维度 | 滑动日志 | 滑动窗口(计数器) |
|---|---|---|
| 精度 | 理论最高(毫秒级) | 取决于子窗口粒度(通常100ms~1s) |
| 内存开销 | O(N),N为窗口内请求数 | O(M),M为子窗口数(固定) |
| CPU开销 | 每次需清理过期数据 | 仅原子计数器增减 |
| 适用场景 | 低频高精度(如风控审核) | 高频低延迟(如交易网关) |
内存爆炸风险:在秒杀场景下,若窗口内请求量达百万级,滑动日志的内存占用可能超过1GB。金融生产环境通常限制滑动日志仅用于白名单或VIP通道。
4.2.3 滑动日志代码示例(Go)
package slidinglog
import (
"sync"
"time"
)
type SlidingLogLimiter struct {
mu sync.Mutex
logs []int64
limit int
windowMs int64
}
func NewSlidingLogLimiter(limit int, window time.Duration) *SlidingLogLimiter {
return &SlidingLogLimiter{
logs: make([]int64, 0, limit*2),
limit: limit,
windowMs: window.Milliseconds(),
}
}
func (l *SlidingLogLimiter) Allow() bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now().UnixMilli()
cutoff := now - l.windowMs
// 移除过期日志
i := 0
for i < len(l.logs) && l.logs[i] < cutoff {
i++
}
l.logs = l.logs[i:]
// 判断是否超限
if len(l.logs) >= l.limit {
return false
}
l.logs = append(l.logs, now)
return true
}
4.3 滑动窗口算法(计数器分片)
滑动窗口通过将时间窗口切分为多个子窗口(bucket),用计数器数组近似模拟滑动效果。这是金融API限流最主流的实现。
4.3.1 原理与分片策略
- 将窗口(如1秒)切为N个子窗口(如10个,每个100ms)。
- 每个子窗口维护一个原子计数器。
- 当前时间落在哪个子窗口,就递增该子窗口计数器。
- 总请求量 = 当前子窗口 + 前(N-1)个子窗口的计数器之和。
- 子窗口过期后自动归零或复用。
4.3.2 精度与内存权衡
| 子窗口数量 | 精度(误差) | 内存占用 | 推荐场景 |
|---|---|---|---|
| 2~5 | ±20%~50% | 极低 | 非关键业务兜底 |
| 10 | ±10% | 低 | 通用交易限流 |
| 20~50 | ±2%~5% | 中等 | 秒杀/抢购 |
| 100+ | ±1%以内 | 较高 | 高频风控审核 |
工程经验:金融交易网关通常选择10~20个子窗口。10个子窗口的误差约±10%,但内存仅为20个int64(约160字节),远低于滑动日志。
4.3.3 滑动窗口状态流转图
4.4 在秒杀/抢购场景的应用
秒杀/抢购对限流有特殊要求:瞬时流量可达正常值的100~1000倍,且要求公平性、防刷单、低延迟。滑动窗口算法在此场景下需做针对性优化。
4.4.1 秒杀限流架构
4.4.2 秒杀场景参数配置
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 全局窗口大小 | 1秒 | 匹配秒杀峰值粒度 |
| 子窗口数量 | 20 | 精度±5%,内存可接受 |
| 用户窗口大小 | 10秒 | 防止快速重试刷单 |
| 用户窗口阈值 | 3~5次 | 正常用户操作频率 |
| 库存预扣窗口 | 500ms | 防止库存超卖 |
4.4.3 避坑指南
- 不要使用滑动日志做秒杀:百万级请求的内存开销会击穿Redis或本地内存。
- 子窗口数量不宜超过50:超过50个后精度提升有限,但内存和计算开销线性增长。
- 用户维度窗口必须独立:全局窗口只能控制总量,无法防止单个用户刷单。
- 库存扣减不要依赖限流窗口:限流只做流量整形,库存扣减需独立原子操作。
生产案例:某支付平台在2023年双十一秒杀中,使用20子窗口的滑动窗口(全局QPS 50万)+ 用户维度10秒5次窗口,成功拦截99.7%的刷单请求,系统零宕机。
4.5 总结
滑动窗口算法在精度与内存之间提供了灵活的权衡空间:
- 滑动日志:精度最高,但内存开销随请求量线性增长,适合低频高精度场景(如风控审核)。
- 滑动窗口(计数器分片):内存固定,精度可通过子窗口数量调节,是金融API限流的主流选择。
- 秒杀/抢购:必须采用“全局窗口 + 用户维度窗口”双层架构,子窗口数推荐20,兼顾精度与性能。