← 所有文章2026.04.24 · COMMERCE · 正式環境電商系統

結帳併發:庫存、餘額預留與狀態轉換

用固定順序鎖定、重新讀取可售量與條件式狀態轉換,處理結帳、付款及取消的競爭。

結帳可能與其他買家、取消操作或逾期排程同時執行。每個請求讀到的資料可能都正確,但合併後的結果仍可能出錯。

目前平台的關鍵邊界是商品可售量、付款帳號的餘額,以及允許消耗預留的狀態。以下範例使用通用名稱,省略客戶實際資料結構。

先鎖定,再讀取可售量

可售量由實體庫存與未付款訂單推算。兩個同時結帳的買家,不能各自預留最後一件商品。

結帳流程先以固定順序鎖定商品,再用另一個查詢讀取可售量。在 PostgreSQL 的 READ COMMITTED 隔離層級下,後一個查詢會在等待鎖結束後取得新的快照,看見前一筆交易剛提交的待付款訂單。

開始交易
  以固定順序鎖定商品
  讀取庫存扣除目前待付款預留後的可售量
  驗證數量並重新計算價格
  預留餘額並建立訂單
提交交易

待付款訂單本身代表庫存預留。付款完成時,在同一個本地交易內扣庫存並改變訂單狀態,讓這些數量在實際消耗庫存時,停止計入待付款預留。

將餘額檢查放進寫入條件

應用程式先讀取的餘額可能已經過期。共用餘額模組會鎖定對應資料列,使用帶條件的更新,並在同一交易內記錄流水。

簡化範例:

UPDATE account_balances
SET available = available - $2,
    held = held + $2
WHERE id = $1 AND available >= $2
RETURNING id;

沒有回傳資料列,代表這次操作沒有取得款項,呼叫端必須停止,不能依照稍早讀取的結果繼續。

餘額身分包含持有帳號、通路與單位。B2B 的付款帳號也可能與下單人不同,必須先釐清責任,再異動款項。

待處理預留只能被認領一次

使用餘額預留的訂單,由付款、取消與逾期共同競爭處理。各路徑以條件更新離開待處理狀態;取得處理權的轉換與帳務異動必須在同一交易內,未取得處理權的一方不能自行扣用或釋放款項。

stateDiagram-v2
  [*] --> 待處理
  待處理 --> 已扣用: 付款
  待處理 --> 已釋放: 取消或逾期

後續退款是另一筆帳務事件,不會把原始付款變回可以重新執行的請求。

清楚界定資料庫保證的範圍

資料庫交易能協調本地紀錄,無法讓外部金流商一起參與該交易。應用在完成付款前,會向金流商核對狀態、金額與幣別,不能只相信瀏覽器傳來的成功訊息。

建單、確認付款及履約的重試也需要分別檢查。一個階段的防重複機制,不代表整條流程都能保證只執行一次。

我透過針對性的資料庫測試與對帳,驗證本地保證:餘額更新有條件限制、付款與退款選到正確帳戶,以及完成或沖回後的相關紀錄能互相吻合。