首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> DB2 資料庫 >> DB2 教學

數據庫 UPDATE 併發陷阱:Java 端計算後更新導致資金賬目不平

     編輯:DB2 教學

血淚案例:10 萬元入賬「憑空消失」的併發 Bug

客戶反饋:「入金審核通過,但可用資金未增加」。排查發現:入金記錄狀態正常,代碼邏輯無誤,日志無異常,**卻偏偏少了一筆 10 萬元**。這類「無錯誤日志、業務數據不一致」是併發 Bug 的典型特徵。

問題代碼:Java 端「讀取-計算-更新」模式

MoneyUser moneyUser = moneyUserMapper.selectByPrimaryKey(uid); // 1. 讀取舊值
moneyUser.setFrozen(moneyUser.getFrozen().subtract(amount));     // 2. 內存計算
moneyUser.setTotalPay(moneyUser.getTotalPay().add(amount));      // 2. 內存計算
moneyUserMapper.updateByPrimaryKey(moneyUser);                   // 3. 全字段更新

併發競態條件演示

兩筆並發出金請求(金額 10、100)同時進入:

時間線程 A (出金 10)線程 B (出金 100)數據庫狀態
T1select → frozen=1000, total=5000select → frozen=1000, total=5000frozen=1000, total=5000
T2計算:frozen=990, total=5010計算:frozen=900, total=5100無變化
T3update 全字段update 全字段以最後提交者為準,丟失一筆變更

結果:數據庫最終僅反映其中一筆交易,另一筆「憑空消失」。

錯誤嘗試:手動控制事務 / 調換 SQL 順序

-- 嘗試 1:先 INSERT 再 UPDATE
START TRANSACTION;
INSERT INTO record VALUES(1,123);
UPDATE money SET money = money + 10; COMMIT;

-- 嘗試 2:先 UPDATE 再 INSERT
START TRANSACTION;
UPDATE money SET money = money + 10;
INSERT INTO record VALUES(1,123); COMMIT;

**結論:事務只能保證單線程原子性,無法解決多線程「讀取-計算-寫回」的競態條件。**

正確解法:數據庫端原子更新(SQL 級別計算)

<update id="transferOut" parameterType="map">
    UPDATE money_user
       SET frozen = frozen - #{amount,jdbcType=DECIMAL},
           total_pay = total_pay + #{amount,jdbcType=DECIMAL}
     WHERE uid = #{uid,jdbcType=INTEGER}
</update>

核心原理對比

維度Java 端計算 + 全字段更新SQL 原子更新
併發安全性❌ 競態條件,丟失更新✅ 行鎖串行化,數學上不可分割
性能一次 SELECT + 一次 UPDATE單次 UPDATE,無額外 SELECT
業務邏輯位置分散在應用層集中在數據層,易審計
適用場景非併發、極簡單邏輯所有資金、庫存、計數器類併發更新

擴展:MySQL/InnoDB 鎖機制保障

UPDATE ... SET col = col + val WHERE pk = ? 會在聚集索引記錄上加 **排他鎖 (X Lock)**,併發事務按順序獲取鎖、執行計算、釋放鎖,天然串行化,無需應用層鎖。

總結與規範

  1. 鐵律: 凡涉及「金額增減、庫存扣減、計數器遞增」的併發更新,**必須在 SQL 中直接運算**,禁止「SELECT 到 Java 計算再 UPDATE」。
  2. 代碼審查重點: 檢查所有 updateByPrimaryKey(entity)、saveOrUpdate(entity) 涉及金額/庫存字段的調用點。
  3. 架構建議: 在 Mapper/XML 或 Repository 層封裝原子更新方法,禁用全實體更新接口。
  4. 補救措施: 已上線系統可通過對賬任務發現不一致,需腳本回滾修復歷史數據。

「領導早就建議在 SQL 裡加運算,我當初沒聽進去」——這句話道盡了無數開發者的遲來覺悟。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved