php樂觀鎖與數據庫事務結合扣除余額失敗:如何確保只扣款一次且數據一致性?
本文分析了在thinkphp6框架下,使用樂觀鎖和數據庫事務機制并發扣除用戶余額時,出現余額扣除不準確或數據不一致的問題,并提供解決方案。問題表現為:有時只扣除一次余額,有時雖然扣除多次,但數據最終不一致。
問題分析:
文中提到的兩種方案都存在缺陷:
-
方案一: 樂觀鎖的驗證(SmsUser::where([‘id’ => $user[‘id’],’balance’ => $oldMoney])->find();)在事務外進行,導致$oldMoney可能在事務開始前已被其他請求修改,樂觀鎖失效,只扣除一次余額。此外,事務范圍過小,只包含余額更新,其他操作(創建訂單、扣除庫存、記錄余額變動)在事務外執行,缺乏原子性保證。
立即學習“PHP免費學習筆記(深入)”;
-
方案二: 將余額更新移到事務外,避免了方案一中的樂觀鎖失效問題。但是,如果事務內任何操作失敗,余額已扣除,但其他操作回滾,造成數據不一致。
根本原因及正確解決方案:
問題的核心在于樂觀鎖和事務的錯誤使用。正確的做法是:
-
數據庫層面實現樂觀鎖: 不要在代碼層面模擬樂觀鎖,而是利用數據庫提供的機制。例如,使用UPDATE sms_user SET balance = newMoney WHERE id = userId AND balance = oldMoney;。這保證了只有當余額與預期一致時才進行更新。
-
將所有相關操作包含在一個事務中: 余額扣除、訂單創建、庫存扣除、余額變動記錄創建等所有相關操作必須在一個事務內執行,確保原子性。事務的成功或失敗將影響所有操作,保證數據一致性。
-
避免手動提交事務: 依賴框架自動管理事務的提交或回滾。如果事務內發生異常,框架會自動回滾,避免數據不一致。
改進后的代碼示例 (偽代碼):
Db::startTrans(); // 開啟事務 try { // 1. 獲取用戶信息和訂單價格 (可能需要加鎖,防止讀取臟數據) $user = SmsUser::lockForUpdate()->find($userId); // 使用數據庫行鎖 $orderPrice = ...; // 2. 驗證余額是否充足 if ($user->balance < $orderPrice) { throw new Exception('余額不足'); } // 3. 更新余額,使用數據庫樂觀鎖 $affectedRows = Db::table('sms_user') ->where('id', $userId) ->where('balance', $user->balance) // 樂觀鎖條件 ->update(['balance' => $user->balance - $orderPrice]); if ($affectedRows === 0) { throw new Exception('樂觀鎖失敗,余額已被修改'); } // 4. 創建訂單、扣除庫存、創建余額變動記錄 // ... Db::commit(); // 提交事務 } catch (Exception $e) { Db::rollback(); // 回滾事務 // 處理異常 }
通過以上改進,可以有效避免只扣款一次以及數據不一致的問題,確保數據安全和業務邏輯的正確性。 記住選擇合適的數據庫鎖機制,例如行鎖或表鎖,以滿足并發控制的需求。 如果并發量極高,可能需要考慮更高級的分布式鎖機制。