API 限流器完整指南:令牌桶與滑動窗口演算法 Node.js 實作教學
你有沒有遇過這種情況:API 上線沒多久,突然被某個用戶端瘋狂打請求,伺服器直接掛掉?或者是爬蟲大軍來襲,正常用戶全部被擋在門外?
這就是為什麼每個 production-grade 的 API 都需要限流(Rate Limiting)。它不只是防禦性措施,更是確保服務品質的基本功。今天我要帶你搞懂兩種最主流的限流演算法——令牌桶(Token Bucket)和滑動窗口(Sliding Window),並且用 Node.js 從零實作。
為什麼需要 API 限流?
限流的目的不只是「擋惡意流量」,還包括:
- 保護後端資源:防止 CPU/記憶體/資料庫被打爆
- 公平性:避免單一用戶佔用所有資源,影響其他人
- 成本控制:雲端計費是按用量的,不限流等於不限花錢
- SLA 保障:確保在高流量下仍能維持承諾的回應時間
如果你之前有設計過API Gateway 微服務閘道,就會知道限流通常是在 Gateway 層做的。但了解底層演算法,你才能正確選擇和調參。
演算法一:令牌桶(Token Bucket)
令牌桶是最經典的限流演算法,概念直覺:
- 有一個「桶」,裡面裝著「令牌(token)」
- 桶有最大容量(burst capacity)
- 系統以固定速率往桶裡加令牌
- 每個請求消耗一個令牌
- 桶空了就拒絕請求
它的優點是允許短時間的突發流量(burst)。桶滿的時候可以一口氣處理大量請求,但長期來看還是會被限制在固定速率。
Node.js 實作
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity; // 桶的最大容量
this.tokens = capacity; // 目前令牌數
this.refillRate = refillRate; // 每秒補充幾個
this.lastRefill = Date.now();
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.capacity,
this.tokens + elapsed * this.refillRate
);
this.lastRefill = now;
}
consume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true; // 允許
}
return false; // 拒絕
}
}
// 使用:每秒 10 個請求,最多突發 20 個
const bucket = new TokenBucket(20, 10);
function rateLimitMiddleware(req, res, next) {
if (bucket.consume()) {
next();
} else {
res.status(429).json({
error: 'Too Many Requests',
retryAfter: Math.ceil(1 / bucket.refillRate)
});
}
}這個實作是全域的。實際上你通常要按用戶限流,這時候需要為每個 API key 或 IP 維護一個獨立的桶:
const buckets = new Map();
function getOrCreateBucket(key) {
if (!buckets.has(key)) {
buckets.set(key, new TokenBucket(20, 10));
}
return buckets.get(key);
}
function perUserRateLimit(req, res, next) {
const key = req.headers['x-api-key'] || req.ip;
const bucket = getOrCreateBucket(key);
if (bucket.consume()) {
next();
} else {
res.status(429).json({ error: 'Rate limit exceeded' });
}
}演算法二:滑動窗口(Sliding Window Log)
滑動窗口的概念是:記錄每個請求的時間戳,然後計算「過去 N 秒內有幾個請求」:
class SlidingWindowLog {
constructor(windowMs, maxRequests) {
this.windowMs = windowMs; // 時間窗口(毫秒)
this.maxRequests = maxRequests; // 窗口內最大請求數
this.logs = new Map(); // key → [timestamps]
}
isAllowed(key) {
const now = Date.now();
const windowStart = now - this.windowMs;
if (!this.logs.has(key)) {
this.logs.set(key, []);
}
const timestamps = this.logs.get(key);
// 清除過期記錄
while (timestamps.length > 0 && timestamps[0] <= windowStart) {
timestamps.shift();
}
if (timestamps.length < this.maxRequests) {
timestamps.push(now);
return true;
}
return false;
}
}
// 60 秒內最多 100 個請求
const limiter = new SlidingWindowLog(60000, 100);滑動窗口的優點是精確——不會有固定窗口邊界的突發問題。但缺點是記憶體消耗大,因為要儲存每個請求的時間戳。
演算法三:滑動窗口計數器(Sliding Window Counter)
這是前兩者的折衷方案,也是 Cloudflare、Nginx 等常用的做法:
class SlidingWindowCounter {
constructor(windowMs, maxRequests) {
this.windowMs = windowMs;
this.maxRequests = maxRequests;
this.counters = new Map(); // key → { prev, curr, prevStart, currStart }
}
isAllowed(key) {
const now = Date.now();
const currWindow = Math.floor(now / this.windowMs);
if (!this.counters.has(key)) {
this.counters.set(key, { prev: 0, curr: 0, window: currWindow });
}
const counter = this.counters.get(key);
if (counter.window < currWindow) {
counter.prev = counter.window === currWindow - 1 ? counter.curr : 0;
counter.curr = 0;
counter.window = currWindow;
}
// 加權計算:前一個窗口的剩餘比例 + 當前窗口
const elapsed = (now % this.windowMs) / this.windowMs;
const estimate = counter.prev * (1 - elapsed) + counter.curr;
if (estimate < this.maxRequests) {
counter.curr++;
return true;
}
return false;
}
}這個方法記憶體消耗小(每個 key 只要兩個計數器),精確度又比固定窗口好。我在生產環境中最推薦這個。
生產環境考量
分散式限流
上面的實作都是單機版。分散式環境下需要用 Redis 做共享狀態:
// 用 Redis 的 INCR + EXPIRE 實作固定窗口
async function redisRateLimit(redis, key, limit, windowSec) {
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, windowSec);
}
return current <= limit;
}回應標頭
符合 RFC 標準的限流回應應該包含這些標頭:
res.set({
'X-RateLimit-Limit': maxRequests,
'X-RateLimit-Remaining': remaining,
'X-RateLimit-Reset': resetTime,
'Retry-After': retryAfter
});跟認證整合
限流通常跟認證搭配使用——未認證的請求限制更嚴格,認證用戶額度更高。之前在OAuth 2.0 第三方登入那篇有提到認證流程,限流可以在認證之後根據用戶等級動態調整。
該選哪種演算法?
| 演算法 | 精確度 | 記憶體 | 允許突發 | 適用場景 |
|---|---|---|---|---|
| 令牌桶 | 中 | 低 | 是 | 允許短期突發的 API |
| 滑動窗口 Log | 高 | 高 | 否 | 精確度要求高的計費 API |
| 滑動窗口計數器 | 中高 | 低 | 部分 | 大多數 Web API(推薦) |
小結
API 限流是後端工程師必備的技能。理解了令牌桶和滑動窗口的原理之後,不管你是要自己實作還是用現成的 library(像 express-rate-limit、rate-limiter-flexible),都能做出正確的配置決策。記住:限流不是可選的,它是任何面向外部的 API 都必須有的基礎設施。
繼續閱讀
REST API 版本控制策略完整教學:URI Path、Header、Query 三種方案實戰比較與最佳實踐
相關文章
REST API 版本控制策略完整教學:URI Path、Header、Query 三種方案實戰比較與最佳實踐
完整比較 REST API 三大版本控制策略:URI Path、Header、Query,含實戰範例、優缺點分析與最佳實踐建議。
OAuth 2.0 第三方登入實作教學:用 Node.js 整合 Google 與 GitHub 登入
每次做專案都要自己刻一套登入系統?用 OAuth 2.0 整合 Google 和 GitHub 第三方登入,不但省時間,使用者體驗也更好。這篇教學從 OAuth 2.0 的核心概念講起,手把手帶你用 Node.js 完成完整實作。
你可能也喜歡
探索其他領域的精選好文