具代表性的面試主題

程式設計面試:實作支援突發流量的 Token Bucket 限流器

程式題中等
Offer.cc 編輯團隊發佈 更新

題幹

請實作一個按使用者限流的 Token Bucket。每個使用者有容量、每秒補充速率和請求成本;給定目前時間,判斷本次請求是否能被允許,並說明並發與邊界處理。

題幹與適用場景

請實作一個按使用者隔離的 Token Bucket 限流器。每個桶有最大容量 capacity、每秒補充速率 refillRate 和目前 token 數;請求攜帶使用者 ID、時間戳和消耗量 cost。如果補充後 token 足夠,扣除成本並允許請求,否則拒絕。需要說明介面設計、複雜度、時間精度、並發安全和測試。

這道題適合後端、平台和基礎設施程式設計面試。AWS 文件把 token bucket 描述為「token 代表請求、按速率補充、每次請求消耗 token」的限流模型,並強調容量和補充速率要透過測試驗證。

面試官考察點

  • 是否能寫出惰性補充公式,而不是用計時器逐桶加 token。
  • 是否使用單調時間,避免牆上時鐘回撥造成 token 增發。
  • 是否限制 token 不超過容量,且拒絕請求時不錯誤扣減。
  • 是否明確單機記憶體狀態與分散式共享狀態的邊界。
  • 是否能覆蓋並發、浮點精度、超大時間間隔和無效參數。

回答前需要釐清的問題

先確認:

  1. cost 是否始終為正整數,還是支援按 token 計費的浮點成本?
  2. 時間戳由呼叫方傳入用於測試,還是限流器自行讀取時鐘?
  3. 只要求單程序實作,還是多個實例必須共享同一使用者配額?
  4. 拒絕時是否需要返回預計可重試時間或剩餘 token?

若沒有補充,可以假設單程序、非負整數成本、奈秒級單調時鐘,允許突發但不能超過桶容量。

30 秒回答框架

我會為每個使用者保存 tokenslastRefillAt,在請求到達時按經過的單調時間惰性補充:min(capacity, tokens + elapsed * refillRate)。確認補充後的 token 不小於 cost 才扣減並返回允許,否則保持狀態並返回拒絕。單機版本用每使用者鎖或原子臨界區保證讀改寫不可分割;分散式版本把同一公式放在支援原子腳本的共享儲存中。參數、時鐘回撥、邊界時間和並發請求都用模型測試驗證。

分步驟深入解答

1. 資料結構與不變量

每個使用者記錄 tokenslastRefillAt 和可選的版本號。狀態必須滿足 0 <= tokens <= capacity,且 lastRefillAt 不倒退。容量和補充速率在建立時驗證為正值;成本必須大於零且不超過容量,否則直接返回參數錯誤,不改變桶狀態。

2. 惰性補充

設目前時間為 now,經過時間為 elapsed = now - lastRefillAt。補充量是 elapsed * refillRate,新 token 為 min(capacity, tokens + refill)。當桶已滿時,仍把 lastRefillAt 推進到 now,避免下一次重複計算同一段時間。使用整數奈秒和有理數計算可減少浮點誤差;若實作使用浮點數,要規定捨入方式並測試長期漂移。

3. 允許與拒絕

補充後若 tokens >= cost,扣除成本並返回允許;否則不扣 token,返回拒絕和可選的 retryAfter。估算重試時間可以用 (cost - tokens) / refillRate,但要向上取整,並處理速率為零或成本超過容量的情況。拒絕結果不應偽造為成功,也不應更新成比目前時間更早的游標。

4. 並發與狀態儲存

單機實作必須把讀取、補充、判斷和寫回放在同一臨界區;按使用者分片鎖可以減少全域鎖競爭。多程序共享配額時,應用內鎖不足,需要 Redis Lua、資料庫行鎖或其他原子交易,把整段狀態轉換作為一次操作。網路重試要攜帶請求 ID,避免客戶端在回應遺失時重複消費業務 token。

5. 過期與容量控制

長期不活躍的使用者狀態應按 lastRefillAt 和租約過期清理,清理不能影響新請求的正確初始化。高基數使用者要有最大狀態數、分片和淘汰策略;淘汰後新桶從滿容量開始可能形成突發,因此生產策略要明確是否允許這種行為。不要用無限 map 讓攻擊者透過偽造使用者 ID 耗盡記憶體。

6. 測試與可觀測性

測試至少覆蓋初始滿桶、連續拒絕、恰好補足、成本等於容量、時間不前進、時間回撥、超長閒置、並發爭用和重複請求。用可控時鐘跑性質測試,持續驗證 token 不越界、成功扣減總量不超過補充總量加初始容量。指標包括允許率、拒絕率、目前 token 分布、狀態數、鎖等待、原子腳本錯誤和記憶體佔用。

高品質示範回答

我會把核心操作定義成 allow(userId, now, cost)。使用者狀態只有 tokenslastRefillAt,每次請求在同一臨界區計算經過時間、補充 token、截斷到容量,再判斷是否足夠。成功時扣除成本並把游標推進到 now;失敗時保留補充後的 token,但不扣除成本。整個過程使用單調時間,狀態不允許回退。

單機用按使用者分片鎖;分散式場景把相同轉換放到 Redis Lua 或資料庫原子交易,不能先讀後寫多個網路往返。請求 ID 只解決重試冪等,不能把限流器誤稱為業務操作的 exactly-once。狀態需要過期清理和高基數保護,避免偽造使用者鍵消耗記憶體。

測試用可控時鐘覆蓋零時間、恰好補足、成本等於容量、超長閒置、時鐘回撥和並發。用性質斷言 token 始終在零到容量之間,允許請求的累計成本不超過初始容量加補充量。上線後觀察拒絕率、token 分布、狀態增長、鎖等待和儲存腳本錯誤;AWS 的實踐也要求先測試擬定限額,再逐步提高。

常見錯誤

  • 為每個桶啟動計時器,導致使用者數增長時執行緒和排程開銷失控。
  • 用牆上時間計算 elapsed,NTP 回撥後產生額外 token。
  • 補充後忘記 min(capacity, ...),讓桶無限增長。
  • 拒絕請求仍扣除 token,造成使用者配額被虛假消耗。
  • 把應用內鎖當成跨實例原子性。
  • 允許 cost > capacity,讓請求永遠等待或返回錯誤的重試時間。
  • 使用浮點數卻不說明捨入與長期漂移。
  • 只測順序呼叫,不測同一使用者的並發讀改寫。

追問及應對

為什麼不用固定視窗計數?

固定視窗實作簡單,但邊界可能允許短時間雙倍突發。Token Bucket 用容量表達允許的突發,用補充速率表達長期速率;如果業務不允許任何突發,再比較滑動視窗或漏桶。

如何返回 Retry-After?

根據缺口除以補充速率並向上取整;速率為零或成本超過容量時返回不可重試或設定錯誤,不返回無限等待的時間戳。

Redis 不可用時怎麼辦?

按介面風險選擇 fail-closed、受限的本地預算或明確降級,並記錄原因。不能讓每個實例都無限放行,也不能把故障偽裝成普通業務拒絕。

修改容量和速率時如何遷移?

先按舊參數補充到變更時刻,再按策略截斷或換算 token,記錄設定版本。降低容量時必須處理現有 token 超過新上限的情況,並保證變更原子化。

如何證明實作沒有超發?

用模型狀態機和隨機時間序列做性質測試,斷言每個時間區間的累計允許成本不超過初始 token 加該區間理論補充量;再用並發壓力測試驗證原子臨界區。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具