引用自 Explain This API 設計 — 如何設計穩定可預測的 API (談冪等性)?
假如使用者在電商網站結帳時,中間網路有一度不穩,使用者等了一下看頁面沒反應,以為自己沒按成功,所以多按了一次結帳,這時不會要讓使用者付兩次款。如果變得要付兩次款,那肯定沒有使用者會想用這個電商的產品。
這時候有些大聰明就會說, 那我就
按鈕點下去時顯示 loading 遮罩讓使用者沒辦法點第二次, 背景一直打 api 檢查有沒有新訂單成功, 直到拿到結果才隱藏 loading 遮罩
這有兩個問題
- Android 的點擊不是馬上會跳到
onClickListener, 也可能使用者快速點了兩次, 觸發兩次onClickListener。 這可以用 RxBinding, FlowBinding, 或類似的 Library 處理。就是把 click event 丟到 Observable/Flow 來 throttle。 - 不保證
檢查訂單的 api 一定會成功, 在 api 沒有確切回應訂單成立以外的狀況, 都不能保證訂單有沒有成立。(例如 api 回應 500, timed out…)
訂購頁就是只會建立一筆訂單 #
對使用者來説, 進到訂購畫面, 不管怎麼手抖連點, 就是只會買一次, 付款一次。
做法應該是
- 進到頁面時, 前端產生一個隨機字串
idempotent key(不要用 UUID, UUID 128 bits 但是用 hex 傳送的時候要 36 bytes, 可以用 Sqid, 或 A-Za-z0-9 shuffle 之後取前幾個字元都比 UUID 好 ) - 打 api 的時候, 把這個
idempotent key一起送過去, 如果整個 api 的流程都跑完, 沒有錯誤, 就把idempotent key跟 訂單編號記在 redis 30 分, 然後把訂單內容回傳 - 如果收到 api 的時候, redis 裡面對應的
idempotent key已經有訂單編號了, 就直接回傳該筆訂單的內容, 不需要重複建立訂單了
實際實作的時候, 也可能有很小機率, 使用者真的點了兩次, 同時送出兩次相同 idempotent key 的 request, 兩個 request 由不同 server 處理, 兩個都認為 redis 沒有紀錄, 因此還是建立了兩次訂單。
這時後才需要
- 用前面說的 RxBinding 限制連點
- 或是 ViewModel 裏面只用一個線程來打 api, 而不是每次都丟到 IO Dispatcher 然後在 Main 等待。
結論 #
如同一開始文章說的, GET/DELETE/PUT 等等 api 都不需要冪等性, 唯獨 POST 要特別注意。
希望看完這篇之後, 後端可以把 API 修好, 讓前端的同仁不用一直辛苦的到處 throttle, loading, 到處新增一堆 api 檢查了。