今天放假有整個早上的時間,決定來整理 Discord 上面大大貼的香香文章:
Avoiding Double Payments in a Distributed Payments System
裡面提到實現最終一致性的三種方式:
- read repair
- write repair
- asynchronous repair
Airbnb 他們在不同的地方都有應用到這三種方式,這篇主要是介紹使用了 write repair 的解決方案。也就是每次客戶端向服務器發出寫入請求時,都會嘗試修復不一致、破損的狀態。
為了讓客戶端能夠自動重試,就需要讓 API 是具有冪等性的:重複發出相同的 Requset,結果也會保持一致。而因為他們需要極低的延遲,不能拆服務出來跑,所以他們弄了一個叫做「奧菲斯(Orpheus)」的 library 來處理這件事
這邊整理了一些我(覺得)可能會遇到的相關內容,有興趣的朋友也可以閱讀原文,文章內有許多圖片來進行說明,值得一讀。
把資料庫操作和網路請求拆到不同階段
整個 API 請求會被重構成三個部分:
- Pre-RPC:把 Request 的詳細資訊存到 DB
- RPC:進行網路請求
- Post-RPC:把 Repsonse 內容、是否成功、能不能重試塞到 DB 裡
因為資料庫已經提供了 ACID,並且只會產生兩種結果(成功或失敗)。因此 Pre-RPC 和 Post-PRC 對資料庫的操作都會包成 Transaction,確保一起成功或失敗。文章內示範了使用 Java 的 Lambda 來把多個操作包在一起(C# 應該用 EFCore 就可以了?)
延伸閱讀:淺談關聯式資料庫與ACID特性
除此之外,他們還嚴格地將資料庫操作和網路互動拆開,藉此降低風險
To maintain data integrity, we adhere to two simple ground rules:
為了保持數據的完整性,我們遵循兩個簡單的基本原則:
-
No service interaction over networks in Pre and Post-RPC phases
預先和後續的RPC階段中,網絡上沒有服務互動
-
No database interactions in the RPC phases
在 RPC 階段中沒有數據庫交互
把錯誤分類為可重試和不可重試
為了確認能不能夠重試,所有的錯誤都要被分類(當然也會遇到一些比較模糊的):
- 伺服器或網路異常這類的,這些錯誤應該是暫時性的,基本上應該要可以重試
- 但如果是退款失敗這種,就需要歸類到不可重試,並且標記起來
In general, we believe unexpected runtime exceptions due to network and infrastructure issues (5XX HTTP statuses) are retryable. We expect these errors to be transient, and we expect that a later retry of the same request may eventually be successful.
一般而言,我們認為由於網路和基礎設施問題而引起的意外運行時異常(5XX HTTP狀態)是可重試的。我們預期這些錯誤是暫時性的,並且我們期望稍後重新嘗試相同的請求可能最終會成功。
We categorize validation errors, such as invalid input and states (for example, you can’t refund a refund), as non-retryable (4XX HTTP statuses) — we expect all subsequent retries of the same request to fail in the same manner. We created a custom, generic exception class that handled these cases, defaulting to “non-retryable”, and for certain other cases, categorized as “retryable”.
我們將驗證錯誤分類為不可重試的錯誤,例如無效的輸入和狀態(例如,無法對退款進行退款),這些錯誤屬於非可重試的類型(4XX HTTP狀態碼)- 我們預期同一請求的所有後續重試都會以相同的方式失敗。我們創建了一個自定義的通用異常類,用於處理這些情況,默認為“不可重試”,對於某些其他情況,則歸類為“可重試”。
……