結帳可能與其他買家、取消操作或逾期排程同時執行。每個請求讀到的資料可能都正確,但合併後的結果仍可能出錯。
目前平台的關鍵邊界是商品可售量、付款帳號的餘額,以及允許消耗預留的狀態。以下範例使用通用名稱,省略客戶實際資料結構。
先鎖定,再讀取可售量
可售量由實體庫存與未付款訂單推算。兩個同時結帳的買家,不能各自預留最後一件商品。
結帳流程先以固定順序鎖定商品,再用另一個查詢讀取可售量。在 PostgreSQL 的 READ COMMITTED 隔離層級下,後一個查詢會在等待鎖結束後取得新的快照,看見前一筆交易剛提交的待付款訂單。
開始交易
以固定順序鎖定商品
讀取庫存扣除目前待付款預留後的可售量
驗證數量並重新計算價格
預留餘額並建立訂單
提交交易
待付款訂單本身代表庫存預留。付款完成時,在同一個本地交易內扣庫存並改變訂單狀態,讓這些數量在實際消耗庫存時,停止計入待付款預留。
將餘額檢查放進寫入條件
應用程式先讀取的餘額可能已經過期。共用餘額模組會鎖定對應資料列,使用帶條件的更新,並在同一交易內記錄流水。
簡化範例:
UPDATE account_balances
SET available = available - $2,
held = held + $2
WHERE id = $1 AND available >= $2
RETURNING id;
沒有回傳資料列,代表這次操作沒有取得款項,呼叫端必須停止,不能依照稍早讀取的結果繼續。
餘額身分包含持有帳號、通路與單位。B2B 的付款帳號也可能與下單人不同,必須先釐清責任,再異動款項。
待處理預留只能被認領一次
使用餘額預留的訂單,由付款、取消與逾期共同競爭處理。各路徑以條件更新離開待處理狀態;取得處理權的轉換與帳務異動必須在同一交易內,未取得處理權的一方不能自行扣用或釋放款項。
stateDiagram-v2
[*] --> 待處理
待處理 --> 已扣用: 付款
待處理 --> 已釋放: 取消或逾期
後續退款是另一筆帳務事件,不會把原始付款變回可以重新執行的請求。
清楚界定資料庫保證的範圍
資料庫交易能協調本地紀錄,無法讓外部金流商一起參與該交易。應用在完成付款前,會向金流商核對狀態、金額與幣別,不能只相信瀏覽器傳來的成功訊息。
建單、確認付款及履約的重試也需要分別檢查。一個階段的防重複機制,不代表整條流程都能保證只執行一次。
我透過針對性的資料庫測試與對帳,驗證本地保證:餘額更新有條件限制、付款與退款選到正確帳戶,以及完成或沖回後的相關紀錄能互相吻合。