#限流算法详解与瞬时超频分析
版本: v1.0
日期: 2026-02-05
目的: 详细解释每种算法的实现原理和瞬时超频特性
#目录
#1. 滑动窗口算法 (Sliding Window)
#📝 算法描述
配置:windowMs: 60000 (60秒), max: 100
说明:同一 IP 在任意连续 60 秒内最多 100 次请求#🔧 实现原理
核心代码(lib/algorithms/sliding-window.js):
async function check(store, key, options) {
const { windowMs } = options;
const now = Date.now();
const windowStart = now - windowMs; // 当前窗口起点
// 获取存储的所有请求时间戳
const data = await store.get(key);
// 过滤:只保留窗口内的请求
const validRequests = data.requests.filter(
(timestamp) => timestamp > windowStart
);
// 计数:窗口内的请求数
const count = validRequests.length;
// 添加当前请求时间戳
validRequests.push(now);
await store.set(key, { requests: validRequests }, windowMs);
return {
count: count + 1, // 包含当前请求
resetTime: validRequests[0] + windowMs, // 最早请求过期时间
};
}#🎯 工作流程
#示例:windowMs: 60000ms (60秒), max: 100
时间轴(毫秒):
0ms 10000ms 20000ms 30000ms ... 60000ms 70000ms
│────────│─────────│─────────│─────────│─────────│─────────│
请求1 请求2 请求3 ... 请求100 请求101?
当前时间:60000ms
窗口范围:[0ms - 60000ms](往前推60秒)
存储数据:
{
requests: [0, 10000, 20000, ..., 59000] // 所有请求的时间戳
}
检查逻辑:
1. now = 60000ms
2. windowStart = 60000 - 60000 = 0ms
3. 过滤:保留 timestamp > 0ms 的请求
4. count = validRequests.length
5. 判断:count + 1 <= max (100) ?#具体场景分析
场景1:正常请求
时间线:
0ms: 请求1 → count=1 ✅ 允许
100ms: 请求2 → count=2 ✅ 允许
200ms: 请求3 → count=3 ✅ 允许
...
59900ms: 请求100 → count=100 ✅ 允许
存储的时间戳:[0, 100, 200, ..., 59900]场景2:瞬时大量请求
假设在 0ms 时刻瞬间发送 100 个请求:
0ms: 请求1 → count=1 ✅ 允许
0ms: 请求2 → count=2 ✅ 允许
0ms: 请求3 → count=3 ✅ 允许
...
0ms: 请求100 → count=100 ✅ 允许
0ms: 请求101 → count=101 ❌ 拒绝(超过 max=100)
在 60000ms(60秒后)之前,所有新请求都会被拒绝。
60001ms: 请求102 → count=1 ✅ 允许(0ms的请求已过期)场景3:跨窗口请求
时间线:
0ms: 请求1-50 (50个请求)
30000ms: 请求51-100(50个请求)
59999ms: 请求101 → count=101 ❌ 拒绝(窗口内有100个)
60001ms: 请求102
→ windowStart = 60001 - 60000 = 1ms
→ 过滤:保留 timestamp > 1ms 的请求
→ 0ms的50个请求被过滤掉
→ count = 50 + 1 = 51 ✅ 允许
90001ms: 请求103
→ windowStart = 90001 - 60000 = 30001ms
→ 30000ms的50个请求被过滤掉
→ count = 1 ✅ 允许#⚡ 瞬时超频分析
#最大瞬时请求数
理论值:max = 100
实际值:max = 100(严格限制)
结论:不允许任何瞬时超频!#超频特性
| 特性 | 值 |
|---|---|
| 瞬时最大请求 | 100(配置的 max 值) |
| 瞬时超频率 | 0%(不允许超频) |
| 窗口边界突发 | 无(连续计算) |
| 精确度 | ⭐⭐⭐⭐⭐(最精确) |
#为什么不允许超频?
// 每次请求都检查"往前推 windowMs"时间内的所有请求
const windowStart = now - windowMs;
const validRequests = data.requests.filter(
(timestamp) => timestamp > windowStart
);
// 严格判断
if (validRequests.length + 1 > max) {
// 拒绝请求
}任意时刻的检查都是动态窗口:
- 0ms 检查:窗口 = [-60000, 0]
- 100ms 检查:窗口 = [-59900, 100]
- 1000ms 检查:窗口 = [-59000, 1000]
每个请求都有独立的"往前看60秒"窗口,所以不存在边界问题。
#📊 内存占用
// 存储结构
{
requests: [timestamp1, timestamp2, ..., timestamp100]
}
// 最坏情况:max=100,每个时间戳8字节
内存占用 ≈ 100 * 8 bytes = 800 bytes per key
// 如果 10000 个用户
总内存 ≈ 10000 * 800 bytes ≈ 7.8 MB#✅ 优点
- ✅ 精确度最高:任意时刻都严格限制
- ✅ 无边界问题:不存在窗口切换时的突发
- ✅ 公平性好:时间分布均匀
#❌ 缺点
- ❌ 内存占用高:需要存储所有时间戳
- ❌ 计算开销大:每次都要过滤数组
- ❌ 不允许突发:瞬时流量被严格限制
#2. 固定窗口算法 (Fixed Window)
#📝 算法描述
配置:windowMs: 60000 (60秒), max: 100
说明:同一 IP 在每个固定的 60 秒窗口内最多 100 次#🔧 实现原理
核心代码(lib/algorithms/fixed-window.js):
async function check(store, key, options) {
const { windowMs } = options;
const now = Date.now();
// 计算当前窗口编号(整除)
const windowKey = Math.floor(now / windowMs);
const fullKey = `${key}:${windowKey}`;
// 直接递增计数器
const result = await store.increment(fullKey, { windowMs });
// 计算重置时间(下一个窗口的开始)
const resetTime = (windowKey + 1) * windowMs;
return {
count: result.count,
resetTime,
};
}#🎯 工作流程
#示例:windowMs: 60000ms (60秒), max: 100
时间轴(毫秒):
0ms 60000ms 120000ms
│─────── 窗口0 ──────────│─────── 窗口1 ───────────│
窗口编号 = floor(now / 60000)
0ms: 窗口0 = floor(0 / 60000) = 0
59999ms: 窗口0 = floor(59999 / 60000) = 0
60000ms: 窗口1 = floor(60000 / 60000) = 1
60001ms: 窗口1 = floor(60001 / 60000) = 1
存储结构:
key:0 → count: 100 (窗口0的计数)
key:1 → count: 50 (窗口1的计数)#具体场景分析
场景1:正常请求
窗口0 [0ms - 60000ms):
0ms: 请求1 → key:0, count=1 ✅
1000ms: 请求2 → key:0, count=2 ✅
...
59000ms: 请求100 → key:0, count=100 ✅
59999ms: 请求101 → key:0, count=101 ❌ 拒绝
窗口1 [60000ms - 120000ms):
60000ms: 请求102 → key:1, count=1 ✅ 允许(新窗口)场景2:窗口边界突发(关键问题)⚠️
时间线:
59000ms - 59999ms: 连续发送 100 个请求
→ 窗口0, count=100 ✅ 全部允许
60000ms - 60999ms: 连续发送 100 个请求
→ 窗口1, count=100 ✅ 全部允许
结果分析:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
在 59000ms - 60999ms(连续 2 秒)内:
实际允许了 200 个请求!
这违反了"60秒内最多100次"的初衷!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
问题原因:
窗口0的最后 1 秒 + 窗口1的开始 1 秒 = 2秒内200次#⚡ 瞬时超频分析
#最大瞬时请求数
理论值:max = 100(单个窗口)
实际最坏情况:max * 2 = 200(跨窗口)
窗口边界时刻的最大瞬时请求数:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
窗口0最后时刻:100 次
窗口1开始时刻:100 次
连续时间内: 200 次(2倍超频!)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#超频特性
| 特性 | 值 |
|---|---|
| 单窗口最大请求 | 100(严格限制) |
| 瞬时最大请求 | 200(窗口边界) |
| 瞬时超频率 | 100%(2倍) |
| 窗口边界突发 | ⚠️ 存在(严重问题) |
| 精确度 | ⭐⭐(边界不精确) |
#超频场景示例
配置:windowMs: 60000ms, max: 100
危险场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间范围:[59000ms, 61000ms](连续2秒)
59000ms - 59999ms: 100次请求 → 窗口0 ✅
60000ms - 60999ms: 100次请求 → 窗口1 ✅
任意连续60秒的统计:
[0ms, 60000ms): 100次 ✅ 符合限制
[59000ms, 119000ms): 200次 ❌ 违反了"60秒100次"
实际效果:允许了 2倍 的请求!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#📊 内存占用
// 存储结构(极简)
{
count: 100 // 只存一个数字
}
// 每个 key 只需 4-8 字节
内存占用 ≈ 8 bytes per key
// 如果 10000 个用户
总内存 ≈ 10000 * 8 bytes ≈ 78 KB#✅ 优点
- ✅ 内存占用最低:只存计数器
- ✅ 计算最快:直接递增
- ✅ 实现简单:逻辑清晰
#❌ 缺点
- ❌ 窗口边界突发:可能瞬时超频 100%(2倍)
- ❌ 精确度差:不符合"任意60秒"的语义
- ❌ 不公平:边界时刻的用户可以发更多请求
#3. 令牌桶算法 (Token Bucket)
#📝 算法描述
配置:capacity: 100 (桶容量), refillRate: 1 (每秒补充1个令牌), windowMs: 1000 (1秒)
说明:同一 IP 最多突发 100 次,平均每秒 1 次(每分钟60次)#🔧 实现原理
核心代码(lib/algorithms/token-bucket.js):
async function check(store, key, options) {
const { capacity = 10, refillRate = capacity, windowMs = 1000, limit = capacity } = options;
const now = Date.now();
const data = await store.get(key);
let tokens = capacity; // 初始桶满
if (data) {
// 计算经过的时间
const timePassed = now - data.lastRefill;
// 计算应该补充的令牌数
const tokensToAdd = (timePassed / windowMs) * refillRate;
// 令牌数 = min(桶容量, 当前令牌 + 新补充的)
tokens = Math.min(capacity, data.tokens + tokensToAdd);
}
// 尝试消耗1个令牌
if (tokens >= 1) {
tokens -= 1;
await store.set(key, { tokens, lastRefill: now }, Math.ceil((capacity / refillRate) * windowMs));
return {
allowed: true,
limit,
current: limit - Math.max(0, Math.floor(tokens)),
remaining: Math.max(0, Math.floor(tokens)),
resetTime: now + Math.ceil(((capacity - tokens) / refillRate) * windowMs),
retryAfter: 0,
};
}
// 没有令牌,拒绝请求
const timeToNextToken = ((1 - tokens) / refillRate) * windowMs;
return {
allowed: false,
limit,
current: limit,
remaining: 0,
resetTime: now + Math.ceil((capacity / refillRate) * windowMs),
retryAfter: Math.ceil(timeToNextToken),
};
}#🎯 工作流程
#示例:capacity: 100, refillRate: 100 (每秒100个), windowMs: 1000
配置含义:
- 桶容量:100 个令牌
- 补充速率:每秒补充 100 个令牌(每10ms补充1个)
- 效果:平均每秒 100 次,允许突发 100 次
令牌桶模型:
┌────────────────────────────┐
│ Bucket (容量: 100) │
│ ┌──────────────────────┐ │
│ │ 🪙🪙🪙🪙🪙🪙🪙 (tokens) │ │
│ └──────────────────────┘ │
│ │
│ ↓ 消耗(每个请求-1) │
│ ↑ 补充(每秒+100) │
└────────────────────────────┘#具体场景分析
场景1:突发流量(关键特性)
初始状态:
tokens = 100(桶满)
0ms: 连续发送 100 个请求
请求1: tokens=100-1=99 ✅
请求2: tokens=99-1=98 ✅
请求3: tokens=98-1=97 ✅
...
请求100: tokens=1-1=0 ✅
请求101: tokens=0<1 ❌ 拒绝
结果:瞬间允许 100 个请求(突发)
10ms: 补充令牌
timePassed = 10ms
tokensToAdd = (10 / 1000) * 100 = 1
tokens = min(100, 0 + 1) = 1
10ms: 请求102 → tokens=1-1=0 ✅ 允许
20ms: 补充令牌 → tokens=1
20ms: 请求103 → tokens=0 ✅ 允许
结论:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0-10ms: 100次(突发)
10-20ms: 1次
20-30ms: 1次
...
平均速率:接近 100次/秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━场景2:长时间空闲后的突发
0ms: 初始 tokens=100
0-60s: 无请求(令牌不增长,已满)
60000ms: 连续发送 150 个请求
tokens = min(100, 100 + 补充) = 100(桶满)
请求1-100: ✅ 允许(消耗桶内令牌)
请求101-150: ❌ 拒绝(桶空)
结论:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
即使长时间空闲,突发请求也不会超过桶容量(100)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━场景3:持续稳定请求
配置:capacity=100, refillRate=100, windowMs=1000
稳定请求:每 10ms 发送 1 个请求(每秒100次)
0ms: 请求1 → tokens=100-1=99 ✅
10ms: 补充1个 → tokens=99+1=100
10ms: 请求2 → tokens=100-1=99 ✅
20ms: 补充1个 → tokens=99+1=100
20ms: 请求3 → tokens=100-1=99 ✅
结果:可以持续稳定在 100次/秒#⚡ 瞬时超频分析
#最大瞬时请求数
理论值:capacity = 100
实际最大瞬时:capacity = 100(桶容量)
关键特性:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
允许突发 capacity 个请求,之后限制在 refillRate
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
示例配置:capacity=100, refillRate=100/秒
- 瞬时允许:100 次(桶满时)
- 之后速率:100 次/秒
- 平均速率:100 次/秒#超频特性
| 特性 | 值 |
|---|---|
| 瞬时最大请求 | 100(桶容量) |
| 持续最大速率 | 100/秒(补充速率) |
| 瞬时超频率 | 0%(不超过桶容量) |
| 突发特性 | ✅ 允许(特色功能) |
| 平滑性 | ⭐⭐⭐(允许突发,之后平滑) |
| 精确度 | ⭐⭐⭐⭐(精确控制平均速率) |
#突发流量计算
配置:capacity=100, refillRate=100, windowMs=1000
情景:长时间空闲后突然大量请求
时刻0: 桶满,tokens=100
时刻0: 瞬间100个请求 → 全部允许,tokens=0
时刻10ms: 补充 = (10/1000)*100 = 1 token
时刻10ms: 1个请求 → 允许
时刻20ms: 补充 = (10/1000)*100 = 1 token
时刻20ms: 1个请求 → 允许
在前 1 秒内:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
允许的请求数 = 桶容量 + 1秒补充的令牌
= 100 + 100 = 200 次
但是!这 200 次不是瞬时的:
- 前 1ms: 100 次(突发)
- 后 999ms: 100 次(匀速补充)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#📊 内存占用
// 存储结构
{
tokens: 99.5, // 当前令牌数(浮点数)
lastRefill: 1643889600000 // 上次补充时间
}
// 每个 key 约 16 字节
内存占用 ≈ 16 bytes per key
// 如果 10000 个用户
总内存 ≈ 10000 * 16 bytes ≈ 156 KB#✅ 优点
- ✅ 允许突发流量:适合有突发需求的场景
- ✅ 平均速率控制:长期看严格限制
- ✅ 用户体验好:不会因短时突发被限制
- ✅ 内存占用低:只存两个数字
#❌ 缺点
- ❌ 瞬时流量大:可能瞬间消耗所有令牌
- ❌ 配置复杂:需要理解 capacity 和 refillRate
- ❌ 不适合严格限制:如果不能接受突发,不推荐
#4. 漏桶算法 (Leaky Bucket)
#📝 算法描述
配置:capacity: 100 (桶容量), leakRate: 100 (每秒漏出100滴), windowMs: 1000 (1秒)
说明:同一 IP 按水位衰减速度恢复额度,桶满时立即拒绝新请求实现边界:当前
leaky-bucket是请求准入控制算法,不维护等待队列,也不会后台匀速派发请求。如果业务需要“排队后按固定速率处理任务”,应结合消息队列或任务调度器。
#🔧 实现原理
核心代码(lib/algorithms/leaky-bucket.js):
async function check(store, key, options) {
const { capacity = 10, leakRate = capacity, windowMs = 1000, limit = capacity } = options;
const now = Date.now();
const data = await store.get(key);
let water = 0; // 桶内水量
if (data) {
// 计算经过的时间
const timePassed = now - data.lastLeak;
// 计算应该漏出的水量
const waterLeaked = (timePassed / windowMs) * leakRate;
// 桶内水量 = max(0, 当前水量 - 漏出的)
water = Math.max(0, data.water - waterLeaked);
}
// 尝试添加水(请求)
if (water + 1 <= capacity) {
water += 1;
await store.set(key, { water, lastLeak: now }, Math.ceil((capacity / leakRate) * windowMs));
return {
allowed: true,
limit,
current: Math.min(limit, Math.ceil(water)),
remaining: Math.max(0, limit - Math.ceil(water)),
resetTime: now + Math.ceil((water / leakRate) * windowMs),
retryAfter: 0,
};
}
// 桶已满,拒绝请求
const timeToLeak = ((water + 1 - capacity) / leakRate) * windowMs;
return {
allowed: false,
limit,
current: limit,
remaining: 0,
resetTime: now + Math.ceil((water / leakRate) * windowMs),
retryAfter: Math.ceil(timeToLeak),
};
}#🎯 工作流程
#示例:capacity: 100, leakRate: 100 (每秒漏100滴), windowMs: 1000
配置含义:
- 桶容量:100 滴水
- 漏出速率:每秒漏出 100 滴(每10ms漏1滴)
- 效果:按漏出速率释放额度,桶满拒绝
漏桶模型:
┌────────────────────────────┐
│ Bucket (容量: 100) │
│ ┌──────────────────────┐ │
│ │ 💧💧💧💧💧 (water) │ │
│ └──────────────────────┘ │
│ ↓ 按时间漏出水位 │
│ │
│ ↑ 添加(每个请求+1) │
└────────────────────────────┘#具体场景分析
场景1:突发流量处理
初始状态:
water = 0(桶空)
0ms: 连续发送 100 个请求
请求1: water=0+1=1 ✅(添加到桶中)
请求2: water=1+1=2 ✅
请求3: water=2+1=3 ✅
...
请求100: water=99+1=100 ✅
请求101: water=100, capacity=100 ❌ 拒绝(桶满)
0-10ms: 桶内有 100 滴水
10ms: 漏出水
timePassed = 10ms
waterLeaked = (10 / 1000) * 100 = 1
water = max(0, 100 - 1) = 99
10ms: 请求102 → water=99+1=100 ✅ 允许(有空间)
20ms: 漏出1滴 → water=99
20ms: 请求103 → water=100 ✅ 允许
结论:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0-1ms: 100次(瞬间填满桶)
1-10ms: 0次(桶满,拒绝)
10-20ms: 1次(漏出1滴,加入1个)
20-30ms: 1次
...
额度恢复速率:100次/秒;容量内请求仍会在到达时立即放行
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━场景2:恒定速率请求
配置:capacity=100, leakRate=100, windowMs=1000
稳定请求:每 10ms 发送 1 个请求(每秒100次)
0ms: 请求1 → water=0+1=1 ✅
10ms: 漏出1滴 → water=1-1=0
10ms: 请求2 → water=0+1=1 ✅
20ms: 漏出1滴 → water=1-1=0
20ms: 请求3 → water=0+1=1 ✅
结果:可以稳定处理 100次/秒,桶内水量稳定在低水位场景3:超速请求
极端场景:每 5ms 发送 1 个请求(每秒200次,超速!)
0ms: 请求1 → water=1 ✅
5ms: 请求2 → water=2 ✅(还未漏出)
10ms: 漏出1滴 → water=2-1=1
10ms: 请求3 → water=2 ✅
15ms: 请求4 → water=3 ✅
20ms: 漏出1滴 → water=3-1=2
20ms: 请求5 → water=3 ✅
持续这样:
水位不断上升 → water: 1, 2, 3, 4, ...
最终 → water=100(桶满)
之后的请求 → ❌ 拒绝
结论:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
漏桶会"削峰":
- 请求速率 > 漏出速率 → 桶逐渐填满 → 最终拒绝
- 请求速率 = 漏出速率 → 桶水量稳定 → 全部允许
- 请求速率 < 漏出速率 → 桶水量下降 → 全部允许
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#⚡ 瞬时超频分析
#最大瞬时请求数
理论值:capacity = 100(桶容量)
实际最大瞬时:capacity = 100
但是!关键区别:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
令牌桶:瞬间100个请求 → 瞬间处理100个 ✅
漏桶:瞬间100个请求 → 容量内立即允许 → 后续按水位衰减恢复额度
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
示例:
瞬间100个请求 → 容量内都立即允许 ✅
后续额度恢复速度 = leakRate = 100/秒
实际效果:
第1秒: 水位按 leakRate 逐步下降并释放新额度
如果桶满:后续请求会在当前请求内立即被拒绝#超频特性
| 特性 | 值 |
|---|---|
| 瞬时最大请求 | 100(桶容量) |
| 额度恢复速率 | 100/秒(按水位衰减计算) |
| 瞬时超频率 | 0%(不超过桶容量) |
| 突发特性 | ⚠️ 容量内立即接受,超出容量立即拒绝 |
| 平滑性 | ⭐⭐⭐⭐(平滑准入,不负责排队处理) |
| 精确度 | ⭐⭐⭐⭐(按时间衰减水位) |
#与令牌桶的区别
令牌桶(Token Bucket):
┌────────────────────────────────────────┐
│ 瞬间100个请求 → 立即处理100个 ✅ │
│ 特点:允许突发 │
└────────────────────────────────────────┘
漏桶(Leaky Bucket):
┌────────────────────────────────────────┐
│ 瞬间100个请求 → 容量内立即允许 │
│ 特点:按漏出速率恢复后续额度 │
└────────────────────────────────────────┘
关键差异:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
令牌桶:请求驱动(有令牌就处理)
漏桶: 水位驱动(按时间衰减水位,释放额度)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#📊 内存占用
// 存储结构
{
water: 50.5, // 当前水量(浮点数)
lastLeak: 1643889600000 // 上次漏出时间
}
// 每个 key 约 16 字节
内存占用 ≈ 16 bytes per key
// 如果 10000 个用户
总内存 ≈ 10000 * 16 bytes ≈ 156 KB#✅ 优点
- ✅ 平滑流量:输出速率恒定
- ✅ 削峰填谷:吸收突发流量
- ✅ 保护后端:防止后端过载
- ✅ 内存占用低:只存两个数字
#❌ 缺点
- ❌ 不适合突发场景:突发请求会被延迟/拒绝
- ❌ 用户体验差:感觉"慢"(相比令牌桶)
- ❌ 配置复杂:需要权衡 capacity 和 leakRate
#5. 算法对比总结
#📊 综合对比表
| 算法 | 瞬时最大请求 | 最大超频率 | 边界问题 | 内存占用 | 计算复杂度 | 突发支持 | 推荐场景 |
|---|---|---|---|---|---|---|---|
| 滑动窗口 | 100 | 0% | 无 | 高 (800B) | 中 | ❌ 不允许 | API限流、精确控制 |
| 固定窗口 | 200 | 100% | ⚠️ 严重 | 低 (8B) | 低 | ⚠️ 边界突发 | 高并发、低精度场景 |
| 令牌桶 | 100 | 0% | 无 | 低 (16B) | 低 | ✅ 允许 | API网关、允许突发 |
| 漏桶 | 100 | 0% | 无 | 低 (16B) | 低 | ⚠️ 平滑处理 | 流量整形、保护后端 |
#🎯 详细对比
#1. 瞬时请求处理
配置:windowMs: 60000ms, max: 100
场景:瞬间发送 150 个请求
滑动窗口:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
请求1-100: ✅ 允许(count <= 100)
请求101-150: ❌ 拒绝(count > 100)
瞬时允许:100 次(严格限制)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
固定窗口:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
窗口0最后1ms: 100次 ✅
窗口1开始1ms: 100次 ✅
瞬时允许:200 次(2倍超频!)⚠️
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
令牌桶:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
桶满,tokens=100
请求1-100: ✅ 瞬间处理(消耗令牌)
请求101-150: ❌ 拒绝(桶空)
瞬时允许:100 次(允许突发)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
漏桶:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
请求1-100: ✅ 立即允许并填满桶(water=100)
请求101-150: ❌ 拒绝(桶满)
之后水位按 leakRate 衰减(每秒100个额度)
瞬时接受:100 次
后续接受:受 leakRate 限制
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#2. 边界场景对比
时间线:[59000ms, 61000ms](连续2秒)
滑动窗口:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
59000ms: 100次请求
→ 窗口 [59000-119000ms]
→ count=100 ✅ 允许
60000ms: 尝试100次请求
→ 窗口 [60000-120000ms]
→ 仍包含59000ms的100次
→ count=200 ❌ 拒绝
任意连续60秒:严格 <= 100 次 ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
固定窗口:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
59000ms: 100次请求 → 窗口0, count=100 ✅
60000ms: 100次请求 → 窗口1, count=100 ✅
连续2秒内:200次 ⚠️
任意连续60秒:可能 <= 200次(不精确)❌
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
令牌桶:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
59000ms: 100次请求 → 消耗100令牌 ✅
60000ms: 100次请求
→ 补充了 (60000-59000)/1000*100 = 100 令牌
→ 消耗100令牌 ✅
连续2秒内:200次 ✅(符合平均速率)
平均速率:100次/秒 ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
漏桶:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
59000ms: 100次请求 → 立即允许,water=100 ✅
59000-60000ms: 漏出100滴
60000ms: 100次请求 → 立即允许,water=100 ✅
连续2秒内:接受200次 ✅
额度恢复速率:100次/秒 ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#🎯 选择指南
#场景1:API限流(推荐:滑动窗口)
需求:严格限制每个用户每分钟100次
推荐:sliding-window
理由:
✅ 精确控制"任意连续60秒"
✅ 无边界问题
✅ 公平性好
❌ 内存占用稍高(可接受)#场景2:高并发场景(推荐:固定窗口)
需求:10万QPS,低延迟,可接受轻微不精确
推荐:fixed-window
理由:
✅ 计算最快
✅ 内存占用最低
✅ 实现简单
⚠️ 边界有突发(可接受)#场景3:API网关(推荐:令牌桶)
需求:允许短时突发,长期限制平均速率
推荐:token-bucket
理由:
✅ 允许突发流量
✅ 平均速率控制
✅ 用户体验好
✅ 适合突发场景(如批量操作)#场景4:保护后端(推荐:漏桶)
需求:平滑流量,防止后端过载
推荐:leaky-bucket
理由:
✅ 恒定输出速率
✅ 削峰填谷
✅ 保护后端系统
❌ 突发请求体验差(trade-off)#📈 性能对比
#内存占用(10000个用户)
滑动窗口:10000 * 800B = 7.8 MB ⭐⭐⭐
固定窗口:10000 * 8B = 78 KB ⭐⭐⭐⭐⭐
令牌桶: 10000 * 16B = 156 KB ⭐⭐⭐⭐⭐
漏桶: 10000 * 16B = 156 KB ⭐⭐⭐⭐⭐#计算复杂度
滑动窗口:O(n) 过滤数组 ⭐⭐⭐
固定窗口:O(1) 直接递增 ⭐⭐⭐⭐⭐
令牌桶: O(1) 简单计算 ⭐⭐⭐⭐⭐
漏桶: O(1) 简单计算 ⭐⭐⭐⭐⭐#精确度
滑动窗口:任意时刻精确 ⭐⭐⭐⭐⭐
固定窗口:窗口内精确,边界不精确 ⭐⭐
令牌桶: 平均速率精确 ⭐⭐⭐⭐
漏桶: 输出速率精确 ⭐⭐⭐⭐#6. 配置建议
#滑动窗口配置
// 严格API限流
const limiter = new RateLimiter({
algorithm: 'sliding-window',
windowMs: 60000, // 60秒
max: 100, // 最多100次
});
// 特点:
// - 任意连续60秒最多100次
// - 瞬时最大:100次
// - 无边界问题#固定窗口配置
// 高并发场景
const limiter = new RateLimiter({
algorithm: 'fixed-window',
windowMs: 60000, // 60秒
max: 100, // 单窗口最多100次
});
// 特点:
// - 单窗口最多100次
// - 瞬时最大:200次(边界)⚠️
// - 最快速度#令牌桶配置
// API网关,允许突发
const limiter = new RateLimiter({
algorithm: 'token-bucket',
capacity: 100, // 桶容量100
refillRate: 100, // 每秒补充100个
windowMs: 1000, // 1秒
max: 100, // 配合使用
});
// 转换为标准配置:
// windowMs: 60000, max: 100
// → capacity: 100, refillRate: 100/60, windowMs: 1000
// 特点:
// - 瞬时最大:100次(桶容量)
// - 平均速率:100次/60秒
// - 允许突发#漏桶配置
// 流量整形,保护后端
const limiter = new RateLimiter({
algorithm: 'leaky-bucket',
capacity: 100, // 桶容量100
leakRate: 100, // 每秒漏出100个
windowMs: 1000, // 1秒
max: 100,
});
// 转换为标准配置:
// windowMs: 60000, max: 100
// → capacity: 100, leakRate: 100/60, windowMs: 1000
// 特点:
// - 瞬时最大:100次(桶容量)
// - 额度恢复速率:100次/60秒
// - 平滑请求准入#7. 总结
#关键结论
| 算法 | 瞬时最大请求数 | 最大超频率 | 适用场景 |
|---|---|---|---|
| 滑动窗口 | max (100) | 0% | 严格限流 ⭐⭐⭐⭐⭐ |
| 固定窗口 | 2×max (200) | 100% | 高并发 ⭐⭐⭐ |
| 令牌桶 | capacity (100) | 0% | 允许突发 ⭐⭐⭐⭐ |
| 漏桶 | capacity (100) | 0% | 平滑流量 ⭐⭐⭐⭐ |
#推荐选择
✅ 默认推荐:滑动窗口(精确、公平、无边界问题)
✅ 高并发: 固定窗口(快速、简单,可接受边界突发)
✅ 允许突发:令牌桶(用户体验好)
✅ 保护后端:漏桶(恒定速率)#📚 相关文档
快速指南:
- 📖 算法对比指南 - 选择决策、使用场景、配置示例
应用配置:
- 📖 配置详解 - algorithm配置和实战场景
返回:
- 📖 文档中心 - 查看所有文档和学习路径
文档版本: v1.0
最后更新: 2026-02-05
维护者: AI Assistant