140 lines
5.2 KiB
Markdown
140 lines
5.2 KiB
Markdown
|
|
---
|
|||
|
|
tags: [redis, rate-limiting, sliding-window-counter, contribution]
|
|||
|
|
create time: 2026-06-01 10:30
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 滑动窗口计数器:加权贡献度详解
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
固定窗口计数器在边界处会出现**突放量达到限制两倍**的问题。滑动窗口计数器的改进思路很直观:**把总窗口切分成 N 个子窗口,用一个加权公式估算当前窗口的总量**。
|
|||
|
|
|
|||
|
|
本节深入讲解它的核心——**贡献度计算**(对应选择题 Q11),以及它在生产中的实际实现方式。
|
|||
|
|
|
|||
|
|
## 一、基本模型
|
|||
|
|
|
|||
|
|
假设总窗口是 1 秒,切成 N=10 个子窗口(每个 100ms):
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD["滑动窗口计数器 - N=10 子窗口"]
|
|||
|
|
subgraph Window["1 秒总窗口"]
|
|||
|
|
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 → 占比 5%/10s = 0.05
|
|||
|
|
W7 (6~7s) : 存活 1.0s → 占比 1.0s/10s = 0.10
|
|||
|
|
W6 (5~6s) : 存活 1.0s → 占比 0.10
|
|||
|
|
...(中间同理)...
|
|||
|
|
W-1(-2~-1s): 存活 0.5s → 占比 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 的路由设计
|