API 設計 — 如何設計穩定可預測的 API (談冪等性)?

2024年10月17日

💎 加入 E+ 成長計畫 與超過 400+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

在之前的文章 API 設計 — 好的 API 設計有什麼特點? 中,我們討論了好的 API 有什麼特點,其中有提到兩點是我們認為需要更多版面來談的,分別是可相容性可預測性。之所以說這兩點特別重要,是因為當沒有處理好,會導致後續很多問題。

在這篇文章中,我們會針對「可預測性」來討論。(完整內容收錄在 E+ 成長計畫當中。)

可預測性:讓 API 穩定可預測

在前後端的世界中,因為溝通不會隨時都是穩定的,例如客戶端的裝置可能出問題、網路可能中間突然斷掉一小段時間,所以在前後端用 API 溝通時,不能只考慮正常的情境 (俗稱 happy path),而是要進一步去考慮出特別的問題時該怎麼處理 (俗稱 edge case)。當能夠去處理各類極端狀況,API 與整個系統將能夠更穩定。

舉例來說,當今天你用一個電子信箱產品,在編寫完草稿後,按下儲存按鈕,不管按一次,還是按十次,都是執行儲存當下的狀態,不會因為你按了十次,中間網路不穩,就存成十份不同的草稿。如果真的是這樣的話,使用者體驗會不太好。

又或者今天假如使用者在電商網站結帳時,中間網路有一度不穩,使用者等了一下看頁面沒反應,以為自己沒按成功,所以多按了一次結帳,這時不會要讓使用者付兩次款。如果變得要付兩次款,那肯定沒有使用者會想用這個電商的產品。

要能夠做到穩定,冪等 (idempotent) 是很關鍵的要點。所謂的冪等性,是指 API 的呼叫或者操作,不論做多少次,都會是相同的結果;或者換個角度說要做到不論請求幾次,API 都不會產生副作用。當能夠做到冪等性,就能夠確保在重試時,不論重試幾次,都能確保只有執行一次,這樣能避免當遇到各類狀況,導致的不必要重複。

事實上,過去業界就有因為沒有處理好 API 冪等,導致造成重大事故的案例。

具體來說,先前 Uber 支付組的工程經理 Gergely Orosz 就曾公開分享,當年 Uber Eats 在印度有個重大事故,是某段時間內,綁定印度最大支付商之一 Paytm 的使用者,即使 Paytm 帳戶沒有餘額,也可以無限地在 Uber Eats 上下單。

Gergely 在分享中談到,會出現這個事故,是當年 Paytm 的 API 做了改動。在改動前,Paytm 的 API 一直是維持冪等的,所以 Uber 的支付團隊在串接時,就預設 API 是冪等的,沒有多做處理。

然而 Paytm 在那次看似無害的改動中,沒有維持 API 的冪等性,這造成的問題是,當 Uber 呼叫 Paytm 的 API 時,第一次因為使用者的餘額不足所以回傳原本預期的錯誤,但這時如果使用者再下一次單,Paytm 會回傳另一種錯誤訊息。

兩次回傳不同的錯誤訊息,看似很無害,但是偏偏因為第二種錯誤訊息原本 Uber 團隊不知道,所以沒處理,因此在 Uber 端就讓這種下單通過。而當使用者發現沒餘額時,只要按兩次就變得能下單成功,當時印度各大學迅速傳開,讓 Uber Eats 在短時間被大量下免費的單,而這造成的商業損失非常可觀。

如何讓 API 有冪等性?

相信看完上面的故事,讀者們已經意識到冪等性的重要。如果 Paytm 的 API 在遇到餘額不足,是穩定回傳 Uber 端可以處理的錯誤訊息。

這時下個問題會是如何讓 API 有冪等性?

以 RESTful API 來說,有些請求相對不用擔心冪等性問題。舉例來說,GET 請求就是,因為假如某個資料存在伺服器,不論請求幾次,資料沒變的狀況下,就會都拿到一樣的資料。

PUT 也是,因為 PUT 是一次修改整個資源,假如有多個請求送來,就以最後送到的請求即可。同樣地 DELETE 刪除某個資源後,就沒有該資源,多發幾個過來的結果都是該資源被刪除,所以也是冪等。

然而,我們很常用的 POST 請求會是相對需要特別處理的。就像電商下訂單時的支付,通常會是用 POST 請求。而最常見的冪等處理方式會是加上冪等鑰 (idempotent key)。所謂的冪等鑰,是一個獨特的 id,讓伺服器端知道這個請求已經被處理過了。

所以如果有網路中斷,或者使用者快速連擊,當同一個請求帶著相同的冪等鑰,伺服器端就知道不用再處理該請求。在系統設計中,遇到追問如何在分散式系統中,避免請求被重複處理,冪等鑰是最基本一定要想到的解法。

舉例來說,全球支付 API 龍頭之一的 Stripe,在 API 設計中,就有冪等鑰的欄位:

Stripe API 文件
Stripe API 文件

具體來說,假如要呼叫 CreatePayment 的 API,客戶端可以先產生一組冪等鑰 (例如用 uuid 來產生),這時如果使用者重複點擊,因為帶著的是同一組冪等鑰,所以伺服器端知道已經處理過,就不會重新處理這個支付請求。

先前 Stripe 有一篇《Designing robust and predictable APIs with idempotency》技術文,深入淺出地談了如何透過冪等鑰來提高 API 可預測性,非常推薦一讀。

閱讀更多

如果你對於「如何設計好 API」這主題感興趣,我們在 E+ 有寫更深入詳細的內容,包含可相容性、如何做好向後相容 (backward compatible)。有興趣的讀者,歡迎加入 E+ 成長計畫。

本文為 E+ 成長計畫的深度內容,截取前三分之一開放免費閱讀。歡迎加入 E+ 成長計畫閱讀完整版本 (點此了解 E+ 的詳細介紹)


API 系列文

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們