設計限流器(Rate Limiter)

問題定義

設計一個 Rate Limiter,控制用戶或服務在時間窗口內的請求數量,保護後端服務不被過量流量壓垮。

ℹ️限流的位置

Rate Limiter 通常放在 API GatewayMiddleware 層,在請求到達業務邏輯之前就做判斷。返回 HTTP 429 Too Many Requests 表示被限流。

需求分析

注意事項

⚠️分散式環境的挑戰

單機限流很簡單(記憶體計數器就好),但多台伺服器時,每台的計數器是獨立的。用戶的請求被分散到不同伺服器,每台都覺得沒超限,實際上總量早就超了。必須用 Redis 等集中式儲存來做分散式限流。

設計流程

1

選擇限流演算法

Token Bucket、Leaky Bucket、Fixed Window、Sliding Window

2

決定限流粒度

按用戶 ID、IP、API Key 或全局

3

設定限流規則

定義每個粒度的速率限制(如每秒 10 次)

4

選擇儲存方案

單機用記憶體,分散式用 Redis

5

整合到 API Gateway

作為 Middleware 在請求處理前攔截

6

回應 Header 設計

返回 X-RateLimit-Remaining 等資訊

架構設計

Token Bucket 演算法(C#)

csharp
public class TokenBucket
{
    private readonly int _capacity;       // 桶的最大容量
    private readonly double _refillRate;  // 每秒填充幾個 token
    private double _tokens;
    private DateTime _lastRefill;
    private readonly object _lock = new();
 
    public TokenBucket(int capacity, double refillRate)
    {
        _capacity = capacity;
        _refillRate = refillRate;
        _tokens = capacity;
        _lastRefill = DateTime.UtcNow;
    }
 
    public bool TryConsume(int tokens = 1)
    {
        lock (_lock)
        {
            Refill();
            if (_tokens < tokens) return false;
            _tokens -= tokens;
            return true;
        }
    }
 
    private void Refill()
    {
        var now = DateTime.UtcNow;
        var elapsed = (now - _lastRefill).TotalSeconds;
        _tokens = Math.Min(_capacity, _tokens + elapsed * _refillRate);
        _lastRefill = now;
    }
}

分散式 Sliding Window(Redis + TypeScript)

typescript
async function isAllowed(
  userId: string,
  windowMs: number,
  maxRequests: number
): Promise<boolean> {
  const now = Date.now();
  const key = `ratelimit:${userId}`;
 
  // Redis pipeline: 原子操作
  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, now - windowMs); // 移除過期紀錄
  pipeline.zadd(key, now, `${now}:${Math.random()}`); // 加入當前請求
  pipeline.zcard(key); // 計算窗口內請求數
  pipeline.expire(key, Math.ceil(windowMs / 1000)); // 設定 key 過期
 
  const results = await pipeline.exec();
  const count = results![2][1] as number;
  return count <= maxRequests;
}
 
// Middleware
app.use(async (req, res, next) => {
  const allowed = await isAllowed(req.userId, 60_000, 100); // 每分鐘 100 次
  if (!allowed) {
    res.set("Retry-After", "60");
    res.status(429).json({ error: "Rate limit exceeded" });
    return;
  }
  next();
});

架構圖

Client
request
API Gateway
check rate
Rate Limiter (Middleware)
read/update counter
Redis (Counters)
Rules Config
Backend API

架構圖解讀:Client 請求先到 API Gateway,Gateway 內嵌的 Rate Limiter Middleware 攔截請求。Middleware 從 Rules Config 載入限流規則(如「用戶級每分鐘 100 次」),然後向 Redis 讀取/更新該用戶的計數器。如果未超限,請求被放行到 Backend API;如果超限,直接回傳 429 給 Client,請求不會到達後端。

實戰補充

💡演算法選擇指南

如何選擇限流演算法?根據你的場景需求:

| 演算法 | 允許突發 | 記憶體用量 | 精確度 | 實作複雜度 | 適合場景 | |--------|---------|-----------|--------|-----------|---------| | Token Bucket | 是(桶滿時可瞬間消耗) | 低(2 個變數) | 中 | 低 | API Gateway 通用限流(最推薦) | | Leaky Bucket | 否(固定速率流出) | 低(佇列) | 中 | 低 | 需要平滑流量的場景(如資料庫寫入) | | Fixed Window | 否 | 極低(1 個計數器) | 低(邊界問題) | 極低 | 粗略限流,對精確度要求不高 | | Sliding Window Log | 否 | 高(存每個請求時間戳) | 最高 | 中 | 需要精確計數且流量不大 | | Sliding Window Counter | 部分 | 低 | 高 | 中 | Fixed Window 精度不夠但 Log 記憶體太大時的折衷 |

經驗法則:80% 的場景用 Token Bucket 就夠了。需要精確計數(如計費 API)用 Sliding Window Counter。需要絕對平滑的輸出速率用 Leaky Bucket。

面試常見問題與參考答案

Q:如果有人用大量不同 IP 發動 DDoS,IP 級限流還有用嗎? A:單獨的 IP 限流不夠,需要多層防禦:IP 限流 + 全局限流 + 行為分析(如正常用戶不會每秒請求同一個 API 100 次)。更上層可以用 WAF(如 Cloudflare)在網路邊緣就擋掉。

Q:限流規則怎麼配置和更新? A:用 Config Service(如 etcd、Consul)集中管理,限流器定期拉取最新規則(如每 30 秒)。這樣不需要重啟服務就能動態調整限流閾值。

理解測驗

🤔 為什麼 Fixed Window Counter 有「邊界問題」?

🤔 在分散式環境中,為什麼不能只用每台伺服器的本地記憶體做限流?

🤔 Token Bucket 演算法中,「桶的容量」代表什麼意義?

重點整理

💡一句話記住

Gateway 攔截、Redis 計數、429 拒絕。 記憶口訣:「攔數拒」— 在 Gateway 攔截、用 Redis 計數、超限就 429 拒絕。演算法八成選 Token Bucket。

| 概念 | 說明 | |------|------| | Token Bucket | 允許突發流量,最常用的演算法 | | Sliding Window | 精確計數,沒有邊界問題 | | HTTP 429 | Rate Limit 超限的標準回應碼 | | Redis | 分散式限流的集中式計數器 | | API Gateway | Rate Limiter 的最佳部署位置 |

你可能也想看

設計短網址服務設計 Key-Value Store

按 ← → 鍵切換課程