---
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 的路由设计