php樂觀鎖與數(shù)據(jù)庫事務(wù)結(jié)合扣除余額:問題分析與解決方案
本文探討在PHP環(huán)境下,使用樂觀鎖和數(shù)據(jù)庫事務(wù)進(jìn)行余額扣除時(shí),如何避免并發(fā)問題導(dǎo)致余額扣除失敗或數(shù)據(jù)不一致的情況。 我們將分析錯(cuò)誤代碼,并提供正確的解決方案。
問題代碼分析及錯(cuò)誤原因:
以下代碼片段試圖通過樂觀鎖和事務(wù)保證并發(fā)環(huán)境下余額扣除的正確性,但存在缺陷:
立即學(xué)習(xí)“PHP免費(fèi)學(xué)習(xí)筆記(深入)”;
錯(cuò)誤代碼片段一:
public function userbuy() { $user = $this->getuser(); $oldmoney = $user['balance']; $orderoffer = $this->getordermoney($orderid); if($oldmoney < $orderoffer['price']) $this->error('賬戶余額不足'); //樂觀鎖方案 (錯(cuò)誤之處) $newmoney = $oldmoney - $orderoffer['price']; $newuser = smsuser::where(['id' => $user['id'],'balance' => $oldmoney])->find(); if(!$newuser) $this->error('用戶不存在'); //開啟數(shù)據(jù)庫事務(wù) db::transaction(function () use($newuser,$orderid,$newmoney){ $newuser->balance = $newmoney; $result = $newuser->save(); if(!$result) $this->error('保存余額失敗'); //創(chuàng)建訂單 code //扣除庫存 code //創(chuàng)建用戶余額變動(dòng)記錄 code db::commit(); //提交事務(wù) (多余操作) }); }
錯(cuò)誤原因:
-
樂觀鎖位置錯(cuò)誤: find() 方法在事務(wù)外部執(zhí)行。多個(gè)并發(fā)請求都獲取到相同的 $oldmoney 值。 只有第一個(gè)請求的 save() 操作成功,后續(xù)請求由于 balance 不再等于 $oldmoney 而失敗。 樂觀鎖的條件判斷應(yīng)該在 update 語句中進(jìn)行。
-
db::commit() 多余: db::transaction() 方法本身會(huì)自動(dòng)提交事務(wù),除非發(fā)生異常回滾。手動(dòng)調(diào)用 db::commit() 冗余且可能掩蓋錯(cuò)誤。
錯(cuò)誤代碼片段二:
public function userbuy() { // ... (代碼同上,省略部分) ... //樂觀鎖方案 (錯(cuò)誤之處) $newUser->balance = $newMoney; $result = $newUser->save(); if(!$result) $this->error('保存余額失敗'); //開啟數(shù)據(jù)庫事務(wù) (錯(cuò)誤之處) Db::transaction(function () use(){ //創(chuàng)建訂單 code //扣除庫存 code //創(chuàng)建用戶余額變動(dòng)記錄 code Db::commit(); //提交事務(wù) (多余操作) }); }
錯(cuò)誤原因:
余額更新操作在事務(wù)外部執(zhí)行。如果后續(xù)操作(創(chuàng)建訂單、扣除庫存等)失敗,余額已經(jīng)更新,導(dǎo)致數(shù)據(jù)不一致。
正確解決方案:
應(yīng)該將樂觀鎖的條件判斷放在數(shù)據(jù)庫 update 語句中,并將所有需要保證原子性的操作都包含在同一個(gè)事務(wù)中。
正確代碼:
public function userbuy() { $user = $this->getuser(); $oldmoney = $user['balance']; $orderoffer = $this->getordermoney($orderid); if ($oldmoney < $orderoffer['price']) $this->error('賬戶余額不足'); $newmoney = $oldmoney - $orderoffer['price']; // 使用數(shù)據(jù)庫的樂觀鎖機(jī)制 (例如mysql) $affectedRows = smsuser::where('id', $user['id']) ->where('balance', $oldmoney) ->update(['balance' => $newmoney]); if ($affectedRows === 0) { $this->error('余額更新失敗,可能存在并發(fā)沖突'); // 樂觀鎖失敗 return; } // 開啟事務(wù),包含所有后續(xù)操作 db::transaction(function () use ($orderid, $newmoney, $user) { // 創(chuàng)建訂單 code // 扣除庫存 code // 創(chuàng)建用戶余額變動(dòng)記錄 code (確保這些操作也包含在事務(wù)中) }); }
這個(gè)解決方案將余額更新和后續(xù)操作都放在同一個(gè)事務(wù)中,并使用數(shù)據(jù)庫提供的樂觀鎖機(jī)制來保證數(shù)據(jù)一致性。如果任何操作失敗,事務(wù)會(huì)自動(dòng)回滾,確保數(shù)據(jù)安全。 避免了手動(dòng)提交事務(wù),簡化了代碼并提高了可讀性。 如果 update 語句影響的行數(shù)為0,則表示樂觀鎖失敗,需要處理并發(fā)沖突。 可以使用重試機(jī)制或其他策略處理此類情況。 具體的樂觀鎖實(shí)現(xiàn)方式取決于使用的數(shù)據(jù)庫系統(tǒng)。