限流算法详解与瞬时超频分析

版本: v1.0
日期: 2026-02-05
目的: 详细解释每种算法的实现原理和瞬时超频特性


目录

  1. 滑动窗口算法 (Sliding Window)
  2. 固定窗口算法 (Fixed Window)
  3. 令牌桶算法 (Token Bucket)
  4. 漏桶算法 (Leaky Bucket)
  5. 算法对比总结

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

✅ 优点

  1. 精确度最高:任意时刻都严格限制
  2. 无边界问题:不存在窗口切换时的突发
  3. 公平性好:时间分布均匀

❌ 缺点

  1. 内存占用高:需要存储所有时间戳
  2. 计算开销大:每次都要过滤数组
  3. 不允许突发:瞬时流量被严格限制

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

✅ 优点

  1. 内存占用最低:只存计数器
  2. 计算最快:直接递增
  3. 实现简单:逻辑清晰

❌ 缺点

  1. 窗口边界突发:可能瞬时超频 100%(2倍)
  2. 精确度差:不符合"任意60秒"的语义
  3. 不公平:边界时刻的用户可以发更多请求

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

✅ 优点

  1. 允许突发流量:适合有突发需求的场景
  2. 平均速率控制:长期看严格限制
  3. 用户体验好:不会因短时突发被限制
  4. 内存占用低:只存两个数字

❌ 缺点

  1. 瞬时流量大:可能瞬间消耗所有令牌
  2. 配置复杂:需要理解 capacity 和 refillRate
  3. 不适合严格限制:如果不能接受突发,不推荐

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

✅ 优点

  1. 平滑流量:输出速率恒定
  2. 削峰填谷:吸收突发流量
  3. 保护后端:防止后端过载
  4. 内存占用低:只存两个数字

❌ 缺点

  1. 不适合突发场景:突发请求会被延迟/拒绝
  2. 用户体验差:感觉"慢"(相比令牌桶)
  3. 配置复杂:需要权衡 capacity 和 leakRate

5. 算法对比总结

📊 综合对比表

算法瞬时最大请求最大超频率边界问题内存占用计算复杂度突发支持推荐场景
滑动窗口1000%高 (800B)❌ 不允许API限流、精确控制
固定窗口200100%⚠️ 严重低 (8B)⚠️ 边界突发高并发、低精度场景
令牌桶1000%低 (16B)✅ 允许API网关、允许突发
漏桶1000%低 (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%平滑流量 ⭐⭐⭐⭐

推荐选择

✅ 默认推荐:滑动窗口(精确、公平、无边界问题)
✅ 高并发:  固定窗口(快速、简单,可接受边界突发)
✅ 允许突发:令牌桶(用户体验好)
✅ 保护后端:漏桶(恒定速率)

📚 相关文档

快速指南

应用配置

返回


文档版本: v1.0
最后更新: 2026-02-05
维护者: AI Assistant