經典架構拆解 · 03 — Stripe API 冪等性設計
前兩篇拆解了「規模怎麼撐」(Netflix)與「即時配對怎麼做」(Uber),這一篇換個主題:正確性。在支付系統裡,「少扣一次」是客訴,「多扣一次」是災難。Stripe 直接在 API 設計這一層就把這問題解掉,是所有在做 B2B API 的工程師都該看一遍的案例。
為什麼值得拆解
在分散式系統裡,「請求只會送達一次」幾乎不可能保證。網路抖動、客戶端重試、負載平衡器超時,任何一個都會讓同一個請求被送出兩次。Stripe 的解法不是「想辦法不重送」,而是讓重送變得安全,這就是冪等性(idempotency)。
對 PCA 考生來說,這個案例直接打中 3 個高頻考點:at-least-once delivery 的處理、webhook retry 策略、事件溯源與 ledger 設計。考題裡只要問到 Pub/Sub 消費者收到重複訊息怎麼辦,底層其實都是 Stripe 這套邏輯。
商業規模與壓力
根據 Stripe 於 2024 年公開的年度信件(Stripe’s 2024 Annual Letter),Stripe 在 2024 年處理超過 1.4 兆美元的總支付金額(Total Payment Volume),且其平台成為多家大型企業的金流基礎設施。根據 Stripe Engineering Blog 於 2017 年的公開文章《Designing robust and predictable APIs with idempotency》,Stripe API 每天處理的 API 請求數級別在億次以上,webhook 也以類似等級對外派送。
在這種規模下,只要有一個不具冪等性的 API 呼叫被重試一次,就可能變成一次重複扣款、一筆建錯的訂閱、一次重複寄出的通知。每一件都是客服成本,也是財報風險。
架構演進簡史
| 年份 | 里程碑 | 意義 |
|---|---|---|
| 2011 | Stripe API 初版上線 | 極度簡單的 REST 設計,成為當年「開發者友善 API」的代名詞 |
| 2013+ | 正式引入 Idempotency-Key header | 讓同一個 key 在 24 小時內重試任意次,結果都一樣 |
| 2017 | 公開發表冪等性設計文章 | 把內部的「upsert with fencing」寫法公開,成為產業標準作法之一 |
| 2017+ | webhook retry 策略公開 | 明確告訴客戶:webhook 會以指數退避(exponential backoff)重試達 3 天 |
| 2020+ | Stripe Workbench、可觀測性(observability)優先 | 所有 API 請求可 replay、追蹤請求 ID,debug 成為產品功能 |
核心技術決策
| 決策 | 為何這樣選 | 替代方案與為何沒選 |
|---|---|---|
所有寫入 API 支援 Idempotency-Key header | 客戶端只要在重試時帶同一個 key,Stripe 就能辨識是重送還是新請求 | 讓客戶端自己做 dedup:每個串接方都重造一次輪子,錯誤率不可控 |
| key 的有效期為 24 小時 | 夠長,涵蓋得了大多數重試情境;又夠短,不用永久佔著儲存 | 永久保留:儲存成本和查詢延遲都會爆掉 |
| webhook 採 at-least-once + 指數退避重試 | 接收端偶爾會掛,不重試等於漏事件;at-least-once 比 at-most-once 安全得多 | 只送一次(at-most-once):任何接收端暫時失敗都會掉事件 |
| 金流狀態以 state machine 明確建模 | PaymentIntent 有明確的 requires_payment_method → requires_confirmation → succeeded 等狀態,每一步可查核 | 用 boolean flag 集合:狀態組合爆炸、無法稽核 |
| observability 視為產品功能 | 客戶自己可以在 Dashboard 看到每個請求、每次 webhook 的完整 trace | 只做內部 log:客戶遇到問題只能開 ticket,支援成本高 |
如果用 GCP 重新蓋
把這套設計搬到 GCP,大致對應如下:
- 冪等性快取(idempotency cache): Firestore 或 Memorystore 存
Idempotency-Key → response映射,TTL 設 24 小時。Firestore 適合跨 region 強一致;Redis 適合超低延遲。 - 支付帳本(ledger): Spanner 存 PaymentIntent 與交易紀錄,利用強一致與 multi-region 能力確保同一 key 的第二次寫入會被拒絕(透過唯一約束或 transactional read-modify-write)。
- webhook 派送: Cloud Tasks 最適合 —— 它原生支援指數退避、最長重試期間(maxRetryDuration)、可設定最大重試次數(maxAttempts),完全符合 Stripe 的 3 天重試模型。失敗用盡重試的任務需自行實作 dead-letter 模式(Cloud Tasks 沒有內建 DLQ)。
- 事件主幹線: Pub/Sub 做內部事件解耦(付款成功 → 通知 / 發票 / 風控),記得消費者端要自行實作 dedup(Pub/Sub 原生支援 dead-letter topic 處理無法消費的訊息)。
- 可觀測性: Cloud Logging + Cloud Trace,搭配 BigQuery 做長期查詢介面給客戶 Dashboard 用。
PCA 考點映射
| 考點 | Stripe 對應 |
|---|---|
| at-least-once 與消費者冪等性 | webhook retry 模型,題目常問「如果訊息被消費兩次怎麼辦」 |
| 非同步 webhook 可靠派送 | Cloud Tasks / Pub/Sub + 重試策略,對應 Stripe webhook |
| 強一致交易 ledger 選型 | Spanner vs Cloud SQL vs Firestore,考點是「為什麼金流要強一致」 |
常見誤解
- 「冪等性就是加一個 UNIQUE 索引」 —— 這只做了一半。完整的冪等性還得「記住上一次的 response 並回放」,不然第二次請求會撞到 UNIQUE 衝突報錯,客戶端以為失敗又重試,就卡進死循環了。
- 「webhook 接收端不用做 dedup」 —— 錯。at-least-once 代表同一個事件可能送達 N 次,接收端一定要拿
event.id去重,不然會把同一筆重複寫進自己的資料庫。 - 「冪等性只是 API 層的技巧」 —— 其實它是整條寫入路徑的設計原則。從 API → 資料庫 → 下游訊息 → webhook,每一段都得想清楚「重送會不會出錯」,少顧一段就會漏。
來源與延伸閱讀
- Stripe API Reference — Idempotent Requests — 官方冪等性 header 規格,公開文件可直接引用。
- Stripe Engineering — Designing robust and predictable APIs with idempotency — 2017 年公開文章,解釋 fencing、key TTL 等設計。
- Stripe Docs — Webhooks best practices — webhook 重試、簽章驗證、冪等消費的官方指引。
- Stripe 2024 Annual Letter — 年度總支付金額引用來源。
- Google Cloud — Cloud Tasks retry configuration — GCP 對應 webhook retry 模型的原生服務。
這篇結束了本系列上半部。下一篇換系列第 4 篇接手:Slack 的即時訊息架構,看他們怎麼用 WebSocket 加 fanout 模型撐起數千萬同時在線的使用者。
🎯 換你練習