Disgn

限流設定太嚴

活動秒殺時,限流設定太嚴導致一堆使用者被攔在外,但系統明明負載還有餘裕

限流規則以「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),可使用「滑動窗口加權平均」法近似將時間劃分為兩個相鄰小窗口。根據目前時間在這兩個窗口的比例,計算加權值

怎麼防止爆量

實務上常「混合使用」

  • 令牌桶 控制瞬間速率
  • 滑動視窗 控制平均速率