5.2 KiB
5.2 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-06-01 10:30 |
滑动窗口计数器:加权贡献度详解
概述
固定窗口计数器在边界处会出现突放量达到限制两倍的问题。滑动窗口计数器的改进思路很直观:把总窗口切分成 N 个子窗口,用一个加权公式估算当前窗口的总量。
本节深入讲解它的核心——贡献度计算(对应选择题 Q11),以及它在生产中的实际实现方式。
一、基本模型
假设总窗口是 1 秒,切成 N=10 个子窗口(每个 100ms):
flowchart TD
subgraph Window["1 秒总窗口(N=10 子窗口)"]
direction LR
W1["W1<br/>已过期的部分 × 占比"]
W2["W2<br/>已过期的部分 × 占比"]
W3["W3<br/>已过期的部分 × 占比"]
W4["W4<br/>当前完整窗口 → 100%"]
end
Current["当前时刻"] --> Pos["位于第 8 个子窗口"]
Pos --> Calc["total = Σ(各子窗口贡献)"]
| 子窗口位置 | 计入比例 | 说明 |
|---|---|---|
| 已过期的前 M-1 个窗口 | 剩余时间比例折算 | count × (剩余未过期时间 / 总窗口大小) |
| 当前正在进行的窗口 M | 100% 全计 | count × 1.0 |
| 未来的窗口 | 0% | 还未发生,不计入 |
[!question]- 💡 思考:为什么要加权?
想象一个场景:某个用户在第 1 秒内发了 100 个请求,但在第 2 秒开始时这些请求"还剩多少"仍然算在当前滑动窗口内?显然是零——因为完全过期了。但如果某个用户在上一个子窗口的末尾发的请求,它只有一小部分时间已经超出当前窗口。这种"活了多少"的差别,就是加权要解决的问题。
二、贡献度计算(核心知识点 🔑)
这是你最薄弱的一环,我们用具体数字来演示。
2.1 Q11 考点:一句话答案
已过期的窗口按其剩余未过期时间占总窗口大小的比例折算贡献度。
例如:总窗口 10s,某个子窗口已过 8s 只剩 2s 仍在窗口内 → 贡献度 = count × 2/10
这在 Q11 选择题中对应答案 C. 按剩余时间比例折算。
2.2 数值演示
让我们看一个完整的例子来加深理解:
总窗口 W = 10s,N = 10 个子窗口,每窗口 1s
当前时刻:t = 7.5s(第 8 个子窗口内,进度 50%)
滑动窗口范围:t ∈ [7.5 - 10, 7.5] = [-2.5, 7.5]
各个窗口在当前窗口内的存活时间:
W8 (7~8s) : 存活 0.5s → 占比 0.5/10 = 0.05
W7 (6~7s) : 存活 1.0s → 占比 1.0/10 = 0.10
W6 (5~6s) : 存活 1.0s → 占比 1.0/10 = 0.10
...(中间同理)...
W₋₁(-2~-1s): 存活 0.5s → 占比 0.5/10 = 0.05
在这个场景中,total = W8.count × 0.05 + W7.count × 0.10 + ... + W-1.count × 0.05
2.3 实际生产实现
在实际代码中,大多数实现维护 N 个槽位(数组),循环覆盖。这样不需要记录复杂的时间线:
// 假设 N=10,每 100ms 一个子窗口
type SlidingWindowCounter struct {
slots [10]int64 // 10 个子窗口
slotIdx int // 当前子窗口索引
windowMs int64 // 总窗口 1000ms
}
func (sw *SlidingWindowCounter) TryAcquire() bool {
now := time.Now().UnixMilli()
// 计算当前属于哪个子窗口
currentSlot := int(now % sw.windowMs / (sw.windowMs / int64(len(sw.slots))))
// 计算滑动窗口内的总请求数
var total float64
for i := 0; i < len(sw.slots); i++ {
if i == currentSlot {
// 当前窗口:100% 计入
total += float64(sw.slots[i])
} else if i < currentSlot {
// 之前的窗口:按相对位置递减权重
diff := currentSlot - i
weight := float64(len(sw.slots)-diff) / float64(len(sw.slots))
total += float64(sw.slots[i]) * weight
}
// i > currentSlot:未来窗口,weight = 0
}
return total < maxCount
}
关键公式:
slot[i] 的贡献度 = count[i] × weight[i]
其中 weight[i] = (len(slots) - (currentSlot - i)) / len(slots),对 i < currentSlot
举例(N=10,当前在第 8 个槽):
- slot[7](上一个完整窗口): weight = (10-1)/10 = 0.9
- slot[6]: weight = (10-2)/10 = 0.8
- slot[0](距当前最远的那个): weight = (10-8)/10 = 0.2
[!note]- 两种实现的等价性
上面的"槽位差值权重"实现和前面"基于时间的剩余比例折算"在数学上是等价的。槽位差值实现是工程上的简化版本,省去了精确时间线的维护,但效果一致——离当前越近的窗口权重越高。
三、三种窗口算法对比
| 维度 | 固定窗口 | 滑动窗口计数器 | 滑动窗口日志 |
|---|---|---|---|
| 精度 | 低(可突破 2×) | 中(≈1/N 误差) | 高(精确到每条) |
| 空间 | O(1) | O(N) 子窗口 | O(maxQPS × window) |
| 突放量 | 2× 阈值 | ≤ (1+1/N)× 阈值 | 精确控制 |
| 适用场景 | 日配额、粗略统计 | 毫秒级精准限流 | 低频次高精场景 |
关联笔记
- 分布式限流 — 完整的限流算法演进路线
- 路由键与Hash Tag — Cluster 环境下 Key 的路由设计