--- tags: [redis, rate-limiting, sliding-window-counter, contribution] create time: 2026-06-01 10:30 --- # 滑动窗口计数器:加权贡献度详解 ## 概述 固定窗口计数器在边界处会出现**突放量达到限制两倍**的问题。滑动窗口计数器的改进思路很直观:**把总窗口切分成 N 个子窗口,用一个加权公式估算当前窗口的总量**。 本节深入讲解它的核心——**贡献度计算**(对应选择题 Q11),以及它在生产中的实际实现方式。 ## 一、基本模型 假设总窗口是 1 秒,切成 N=10 个子窗口(每个 100ms): ```mermaid flowchart TD subgraph Window["1 秒总窗口(N=10 子窗口)"] direction LR W1["W1
已过期的部分 × 占比"] W2["W2
已过期的部分 × 占比"] W3["W3
已过期的部分 × 占比"] W4["W4
当前完整窗口 → 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 个槽位(数组),循环覆盖。这样不需要记录复杂的时间线: ```go // 假设 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 的路由设计