Rate Limiter

限流設定太嚴
活動秒殺時,限流設定太嚴導致一堆使用者被攔在外,但系統明明負載還有餘裕
限流規則以「API層」為單位,還是以「使用者動作意圖」為單位
深層解析
多數限流設計只看 API 請求頻率(例如每秒可呼叫幾次),但購物行為其實是「任務導向」的。
同樣的 API,在不同情境下(加購、改數量、結帳前檢查)代表不同優先度。
若只看技術層頻率,會錯殺真實用戶。
這是「技術限流」與「業務語意限流」的衝突。
具體例子
A 用戶在秒殺活動中加購一件商品,B 用戶在修改購物車,兩者都觸發相同限流規則,但其實 A 的行為應該更被優先處理。
限流的錯誤回饋,是「資訊透明」還是「行為引導」
目前許多限流回饋只是彈窗:「因搶購熱烈,請稍後再試」。
但這會導致用戶亂重試、加重流量。
若能在訊息中加入「重試建議」或「倒數回應」,就能把限流轉化為節流(引導行為)。
這是「被動防守」與「主動引導」的矛盾。
具體例子
使用者被告知「請稍後重試」不如告訴他「系統將於3秒後自動重試」或「已為你保留名額30秒」。
限流策略是否考慮跨服務依賴(例如庫存、票匣、購物車)之間的同步
深層解析
目前流程是線性檢查(庫存 → 全店票匣 → SKU票匣 → 加入購物車),這種設計容易在高併發下出現「前面通過、後面失敗」的現象。
這是分散式一致性與限流節點分散的結構矛盾。
若不整合限流決策層,系統會變成「每個子系統都在限流,但沒人知道總體負載」。
具體例子
庫存API放行了1000個請求,但購物車服務只能處理500個,導致一半用戶在最後階段被拒。
限流決策邏輯:選哪些 API 要限
💡 原則:限「風險高又難回滾」的 API
不是所有 API 都需要限流。應該依據 API 的「流量衝擊風險 × 業務敏感性」分級管理
| 風險級 | API 類型 | 是否要限 | 限流目標 | 說明 |
|---|---|---|---|---|
| 🔴 高 | 加入購物車 (AddToCart)、下單 (CreateOrder)、庫存扣減 (ReserveStock)、領券 (ClaimCoupon) | ✅ 強限 | 防止超賣、避免 DB 熔毀 | 實際改變狀態,且秒殺時併發高 |
| 🟡 中 | 商品詳情、庫存查詢、購物車列表、折扣試算 | ⚙️ 軟限 | 保持 API 穩定 | 可緩存、可重試 |
| 🟢 低 | 搜尋、Banner、登入、追蹤點擊 | ❌ 不限或緩存 | 提升體驗 | 這些屬讀取型、不怕瞬間流量 |
限流策略邏輯:怎麼限?限誰
設計限流要從三個角度去想:「誰在打」、「打什麼」、「打多快」
按「主體(Who)」限流
| 主體 | 實作 Key | 應用場景 | 範例 |
|---|---|---|---|
| 使用者 | user:{id} |
防止單一帳號濫用 | 同一人秒刷 AddToCart |
| IP 或裝置 | ip:{addr} / device:{id} |
匿名或機器人防禦 | 同 IP 同時打太多請求 |
| 全站 | global |
全域防護 | 當促銷高峰 CPU 達閾值時降流 |
建議做多層策略: Global → User → SKU 依序檢查,一層不過就拒絕
按「資源(What)」限流
| 資源 | 實作 Key | 範例 | 限制邏輯 |
|---|---|---|---|
| SKU | sku:{id} |
熱銷商品 A | 每 SKU 每秒最多 100 次 AddToCart |
| 活動 | campaign:{id} |
11.11 搶購活動 | 每活動每秒 1000 次 |
| 購物車 | cart:{userId} |
同一購物車操作 | 限同一購物車的同時請求數 |
防止熱門資源被同時搶爆
按「動作(How Fast)」限流
| 模型 | 適用場景 | 說明 |
|---|---|---|
| Token Bucket(令牌桶) | 穩定限速,允許突發 | 最常用於 AddToCart |
| Leaky Bucket(漏桶) | 平滑輸出,防尖峰 | 適用於庫存服務 |
| Sliding Window(滑動窗) | 精確統計時間窗 | 適合高併發活動場景 |
實務建議
- AddToCart 用 Token Bucket
- ReserveStock 用 Sliding Window
- 全站限流用 Leaky Bucket
熱配置
把每個 API 的限流策略放在設定中心(Consul、Redis、Config Server)。可即時下發新參數(limit, window, burst)
{
“RateLimits”: {
“/cart/add”: { “limit”: 5, “window”: 1, “by”: “user+sku” },
“/order/create”: { “limit”: 2, “window”: 5, “by”: “user” },
“/coupon/claim”: { “limit”: 1, “window”: 60, “by”: “user+coupon” }
}
}
指標監控
rate_limit_reject_total{api=”/cart/add”}
rate_limit_wait_time_avg
rate_limit_retry_success_rate
讓你知道:限流是不是太嚴、太鬆、或根本沒起作用
動態調整
可根據系統資源自動調整閾值
CPU > 80% → 降低限流閾值
平均延遲 > 500ms → 降速
QueueLength < 10% → 放寬
如果沒限流
1️⃣ API 雪崩效應(Cascade Failure)
沒有限流,當高峰瞬間湧入請求(像秒殺、雙11),每個服務都拼命嘗試處理全部流量
→ 結果 CPU 滿載、Thread Pool 塞滿、延遲爆炸
A 服務撐不住 → 影響 B 服務(下游庫存、訂單都被拖垮)
因為重試請求疊加,導致流量呈「倍數反彈」
最後整個系統「看似活著,其實沒在回應」
資料不一致(Over-selling / Double Booking)
沒有限流,可能有上千個請求同時搶同一個庫存。
若後端沒辦法同步鎖控,就會出現:
賣出超過庫存(over-sell)
訂單重複
支付成功但庫存為負
這不只是系統錯,而是商業信用損失。
上游穩定,下游撐不住
沒有限流時,前台或 API Gateway 會把所有請求丟到後端。
結果:
購物車服務撐不住
庫存或支付系統延遲 → 超時 → 重試 → 加倍負載
最終整個叢集陷入「連鎖阻塞(Thread starvation)」
這時即使你加機器,也來不及——因為整個 Thread Pool 都被卡死
FixedWindowRateLimiter(固定視窗限制)
允許限制,例如“每分鐘 60 個請求”。每分鐘可以發出 60 個請求,即每秒一個,也可以一次性發出 60 個
瞬時爆量
如果在固定視窗的起始點瞬間發出 60 個請求,然後下一個視窗又立即允許 60 個,會不會導致「瞬時爆量」問題,失去了限制流量的本意? 固定視窗雖然「總量」受限,但時間邊界的切割是人工的。系統在兩個視窗交界處可能短時間內接收到超過設計負載的請求,這讓表面的「控制」其實不完全穩定。假設限制是每分鐘 60 次,使用者在 00:59 發出 60 次,在 01:00 又立刻發出 60 次,那在短短 2 秒內系統就收到 120 次請求
「固定」的時間單位是方便系統,而非自然反映使用行為。真正的公平與效率需要根據使用模式調整,例如使用滑動視窗或令牌桶演算法,例如有的使用者可能每隔 10 秒穩定請求一次,有的則會在一分鐘內集中請求。對前者來說限制太嚴,對後者來說反而太寬鬆
==> 可以改用 滑動視窗(Sliding Window) 或 令牌桶(Token Bucket)。這樣限制的是「平均速率」而非「時間片段內的總量」,能自然平滑請求分佈。或使用令牌桶演算法自然控制「瞬間流量」與「平均流量」:瞬間不超過桶容量且平均流量受補充速率控制,可在分散式環境中透過 Redis 共享桶狀狀態。
視窗邊界不一致
如果固定視窗跨越系統時區或分散式部署環境,視窗邊界如何一致?是否可能出現節點間不同步的矛盾
分散式系統難以保證各節點時間完全一致。若每個節點以自己本地時間計算視窗,可能導致整體限制失準。
假設 A 節點的視窗在 12:00 重置,B 節點因時差或延遲在 12:00:30 才重置,那使用者輪流打兩個節點就能突破限制
可設計「動態令牌桶」演算法,根據歷史請求頻率自動調整令牌補充速率,結合集中式儲存(如 Redis、DynamoDB)統一記錄狀態,兼顧穩定使用者的公平性與突發流量的彈性。
SlidingWindowRateLimiter
更高的記憶體成本
這是典型的「精度與成本」矛盾。滑動視窗需要儲存所有請求時間戳才能動態計算,而固定視窗只需維護一個計數器。這代表精確的限制需要更高的計算與記憶開銷。
如果每秒有上千使用者同時打 API,滑動視窗的 deque 可能儲存成千上萬筆時間資料,影響效能,具體且完整的解法說明與程式案例(Sliding Window Counter Approximation),可使用「滑動窗口加權平均」法近似將時間劃分為兩個相鄰小窗口。根據目前時間在這兩個窗口的比例,計算加權值
怎麼防止爆量
實務上常「混合使用」
- 令牌桶 控制瞬間速率
- 滑動視窗 控制平均速率


