iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Vibe Coding

做一個團購後端,順便搞懂那些事系列 第 17 篇

Day 17|CAS:兩台機器同時扣款會發生什麼事?

  • 分享至 

  • xImage
  •  

結算團購訂單時,系統需要從每位參加者的帳戶餘額中扣除應付金額。

User user = userMapper.selectById(userId);

if (user.getBalance() < amount) {
    throw new InsufficientBalanceException("餘額不足");
}

user.setBalance(user.getBalance() - amount);
userMapper.updateById(user);

如果系統只有一台機器,而且結算工作確定會依序執行,這段程式通常不會立刻出問題,但如果未來部署成多台機器,同一時間可能有兩個結算工作處理同一個使用者的訂單,就會出現race condition。

情境:同時對同一個帳號進行扣款

假設小明的餘額是 500,同一時間內,訂單 A 要扣 300、 訂單 B 要扣 400,兩台機器幾乎同時執行,兩邊都以為自己拿到了最新的餘額,因此都通過檢查,最後資料庫卻可能只剩 100。

https://ithelp.ithome.com.tw/upload/images/20260927/20168667i7biHMHuwc.png

實際上兩筆訂單應該總共扣掉 700,但帳戶卻只從 500 被扣成 100,其中一次更新把另一次更新的結果覆蓋掉了。

這次專案使用 CAS(Compare-And-Swap)的概念處理,把「餘額必須足夠」直接放進 UPDATE 的條件。

UPDATE users
SET balance = balance - #{amount}
WHERE user_id = #{userId}
  AND balance >= #{amount}

這條 SQL 同時做了兩件事:「如果這個使用者的餘額足夠,就扣款;否則不要更新。」

這樣子做兩個請求仍然可以同時抵達資料庫,但針對同一筆 users 資料的更新會受到資料庫併發控制。

https://ithelp.ithome.com.tw/upload/images/20260927/20168667H7G4LT2W3U.png

為什麼不用 Optimistic Lock 解決?

看到 WHERE ... AND ... 這種寫法,很容易聯想到 Optimistic Lock,兩者確實有相似之處:都是把某個條件放進 UPDATE,讓更新只有在條件成立時才成功,但兩者比較的東西不同。

下面是 Optimistic Lock的舉例:

UPDATE categories
SET name = #{name},
    version = version + 1
WHERE category_id = #{categoryId}
  AND version = #{version}

這裡不是在判斷「分類名稱是否還符合某個業務規則」,而是在確認:「我讀取資料時看到的版本,現在還是不是同一個版本?」

套用到這次race condition的情境,如果用上面版本比對的方式處理,會變成第二筆請求進來時,即使餘額足夠扣款,卻因為版號不同而被擋下來,所以這個做法這次在這裡並不適用。


上一篇
Day 16|店家改價之後,已經下的單要扣多少?
系列文
做一個團購後端,順便搞懂那些事 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言