類似購物的程式,程式上的流程是這樣的:
1、使用者發起請求,下單
2、檢查各種參數是否齊全、有效
3、檢查使用者餘額是否足夠
4、寫入訂單表
5、寫入使用者表,將使用者餘額減少
6、寫入記錄表,記錄使用者下單買的啥,以及花了多少錢
今天發現一個神奇的使用者,他在1秒鐘之內下了20單!至於是不是1秒鐘無從查起,因為資料庫只精確到秒。
更奇怪的是:
1、明明沒有足夠的餘額,卻繼續進入了後續的步驟
2、寫入訂單表成功、寫入記錄表成功,但是就是沒有扣餘額
我想來想去也沒弄明白這是怎麼回事兒,各位遇到過嗎?有何應對方法?
** 其他使用者是完全正常的,只有這個瞬間下很多單的不正常。
public function orderCreate(Request $request, Response $response) { if(!$user = session('wechat.oauth_user')){ return response()->json([ 'error' => '身份驗證失敗,請重新打開頁面再試' ]); } if(is_null($request->input('object', NULL)) || is_null($request->input('stake', NULL)) || is_null($request->input('time', NULL)) || is_null($request->input('direction', NULL))){ return response()->json([ 'error' => '參數提交不全,請重新打開頁面再試' ]); } if($request->input('stake') != 20 && $request->input('stake') != 50 && $request->input('stake') != 100 && $request->input('stake') != 200 && $request->input('stake') != 500 && $request->input('stake') != 1000 && $request->input('stake') != 2000 && $request->input('stake') != 3000){ return response()->json([ 'error' => '參數提交錯誤,請重新打開頁面再試' ]); } if($request->input('time') != 60 && $request->input('time') != 120 && $request->input('time') != 180 && $request->input('time') != 240 && $request->input('time') != 300){ return response()->json([ 'error' => '參數提交錯誤,請重新打開頁面再試' ]); } if($request->input('direction') != 1 && $request->input('direction') != 0){ return response()->json([ 'error' => '參數提交錯誤,請重新打開頁面再試' ]); } if(!$object = Object::find($request->input('object'))){ return response()->json([ 'error' => '參數提交錯誤,請重新打開頁面再試' ]); } $object_latestPrice = Price::where('id_object', $object->id)->orderBy('created_at', 'desc')->first(); if((strtotime($object_latestPrice->body_price_time) + 300) < time()){ return response()->json([ 'error' => '休市期間無法進行交易' ]); } if(!$user = User::where('id_wechat', $user->id)->first()){ return response()->json([ 'error' => '身份驗證失敗,請重新打開頁面再試' ]); } if(floatval($user->body_balance) < $request->input('stake')){ return response()->json([ 'error' => '帳戶可用餘額不足,請先儲值後再交易' ]); } if($user->is_disabled > 0){ return response()->json([ 'error' => '帳戶已被封鎖,無法進行交易' ]); } $order = new Order; $order->id_user = $user->id; $order->id_object = $object->id; $order->body_price_buying = $object_latestPrice->body_price; $order->body_stake = $request->input('stake'); $order->body_bonus = $object->body_profit * $request->input('stake'); $order->body_direction = $request->input('direction'); $order->body_time = $request->input('time'); $order->save(); $user->body_balance = floatval($user->body_balance) - floatval($order->body_stake); $user->body_transactions = floatval($user->body_transactions) + floatval($order->body_stake); $user->save(); $record = new Record; $record->id_user = $user->id; $record->id_order = $order->id; $record->body_name = $request->input('direction') == 1? '買入看漲' : '買入看跌'; $record->body_direction = 0; $record->body_stake = $order->body_stake; $record->save(); return response()->json([ 'result' => $order->toArray() ]); }
UPDATE:
現在在一大堆的條件判斷之後,希望改成事物來處理這件事,但是 Laravel 的事務這麼寫正確嗎?或者說我這麼寫的話能夠起到我想要的作用嗎?有點懵 - -
DB::beginTransaction(); $user->body_balance = floatval($user->body_balance) - $request->input('stake'); $user->body_transactions = floatval($user->body_transactions) + $request->input('stake'); $user->save(); if($user->body_balance < 0) { DB::rollback(); } else { $order = new Order; $order->id_user = $user->id; $order->id_object = $object->id; $order->body_price_buying = $object_latestPrice->body_price; $order->body_stake = $request->input('stake'); $order->body_bonus = $object->body_profit * $request->input('stake'); $order->body_direction = $request->input('direction'); $order->body_time = $request->input('time'); $order->save(); $record = new Record; $record->id_user = $user->id; $record->id_order = $order->id; $record->body_name = $request->input('direction') == 1? '買入看漲' : '買入看跌'; $record->body_direction = 0; $record->body_stake = $order->body_stake; $record->save(); $this->computeNetwork($user, $order); if($order->body_time == 60) $this->computePrice($user, $order, $object); } DB::commit();
回複內容:
類似購物的程式,程式上的流程是這樣的:
1、使用者發起請求,下單
2、檢查各種參數是否齊全、有效
3、檢查使用者餘額是否足夠
4、寫入訂單表
5、寫入使用者表,將使用者餘額減少
6、寫入記錄表,記錄使用者下單買的啥,以及花了多少錢
今天發現一個神奇的使用者,他在1秒鐘之內下了20單!至於是不是1秒鐘無從查起,因為資料庫只精確到秒。
更奇怪的是:
1、明明沒有足夠的餘額,卻繼續進入了後續的步驟
2、寫入訂單表成功、寫入記錄表成功,但是就是沒有扣餘額
我想來想去也沒弄明白這是怎麼回事兒,各位遇到過嗎?有何應對方法?
** 其他使用者是完全正常的,只有這個瞬間下很多單的不正常。
public function orderCreate(Request $request, Response $response) { if(!$user = session('wechat.oauth_user')){ return response()->json([ 'error' => '身份驗證失敗,請重新打開頁面再試' ]); } if(is_null($request->input('object', NULL)) || is_null($request->input('stake', NULL)) || is_null($request->input('time', NULL)) || is_null($request->input('direction', NULL))){ return response()->json([ 'error' => '參數提交不全,請重新打開頁面再試' ]); } if($request->input('stake') != 20 && $request->input('stake') != 50 && $request->input('stake') != 100 && $request->input('stake') != 200 && $request->input('stake') != 500 && $request->input('stake') != 1000 && $request->input('stake') != 2000 && $request->input('stake') != 3000){ return response()->json([ 'error' => '參數提交錯誤,請重新打開頁面再試' ]); } if($request->input('time') != 60 && $request->input('time') != 120 && $request->input('time') != 180 && $request->input('time') != 240 && $request->input('time') != 300){ return response()->json([ 'error' => '參數提交錯誤,請重新打開頁面再試' ]); } if($request->input('direction') != 1 && $request->input('direction') != 0){ return response()->json([ 'error' => '參數提交錯誤,請重新打開頁面再試' ]); } if(!$object = Object::find($request->input('object'))){ return response()->json([ 'error' => '參數提交錯誤,請重新打開頁面再試' ]); } $object_latestPrice = Price::where('id_object', $object->id)->orderBy('created_at', 'desc')->first(); if((strtotime($object_latestPrice->body_price_time) + 300) < time()){ return response()->json([ 'error' => '休市期間無法進行交易' ]); } if(!$user = User::where('id_wechat', $user->id)->first()){ return response()->json([ 'error' => '身份驗證失敗,請重新打開頁面再試' ]); } if(floatval($user->body_balance) < $request->input('stake')){ return response()->json([ 'error' => '帳戶可用餘額不足,請先儲值後再交易' ]); } if($user->is_disabled > 0){ return response()->json([ 'error' => '帳戶已被封鎖,無法進行交易' ]); } $order = new Order; $order->id_user = $user->id; $order->id_object = $object->id; $order->body_price_buying = $object_latestPrice->body_price; $order->body_stake = $request->input('stake'); $order->body_bonus = $object->body_profit * $request->input('stake'); $order->body_direction = $request->input('direction'); $order->body_time = $request->input('time'); $order->save(); $user->body_balance = floatval($user->body_balance) - floatval($order->body_stake); $user->body_transactions = floatval($user->body_transactions) + floatval($order->body_stake); $user->save(); $record = new Record; $record->id_user = $user->id; $record->id_order = $order->id; $record->body_name = $request->input('direction') == 1? '買入看漲' : '買入看跌'; $record->body_direction = 0; $record->body_stake = $order->body_stake; $record->save(); return response()->json([ 'result' => $order->toArray() ]); }
UPDATE:
現在在一大堆的條件判斷之後,希望改成事物來處理這件事,但是 Laravel 的事務這麼寫正確嗎?或者說我這麼寫的話能夠起到我想要的作用嗎?有點懵 - -
DB::beginTransaction(); $user->body_balance = floatval($user->body_balance) - $request->input('stake'); $user->body_transactions = floatval($user->body_transactions) + $request->input('stake'); $user->save(); if($user->body_balance < 0) { DB::rollback(); } else { $order = new Order; $order->id_user = $user->id; $order->id_object = $object->id; $order->body_price_buying = $object_latestPrice->body_price; $order->body_stake = $request->input('stake'); $order->body_bonus = $object->body_profit * $request->input('stake'); $order->body_direction = $request->input('direction'); $order->body_time = $request->input('time'); $order->save(); $record = new Record; $record->id_user = $user->id; $record->id_order = $order->id; $record->body_name = $request->input('direction') == 1? '買入看漲' : '買入看跌'; $record->body_direction = 0; $record->body_stake = $order->body_stake; $record->save(); $this->computeNetwork($user, $order); if($order->body_time == 60) $this->computePrice($user, $order, $object); } DB::commit();
沒見過涉及金錢交易不開事務就執行的,請用事務解決此類問題。
更新一下:
有人回答先扣錢就行,答案是否定的,在MySQL中不用事務一定完成不了這個操作。
舉個不用事務先扣錢的例子,
收到請求A,進行餘額查詢,餘額足夠,
這時候請求B闖入,也進行了餘額查詢,餘額足夠,
請求A開始更新喻額,然後進行了其他動作,
請求B也開始更新喻額,進行其他動作。
如此一樣解決不了並發的問題。
事務加一,而且優先判斷金額等重要條件
沒看懂你代碼具體的實現,但是我猜你可能取到的髒資料。
你可以試試如下方案
trans begin
sql:update xxx set 帳戶餘額 = 帳戶餘額 - 消費金額(計費操作)
sql:select 帳戶餘額 from xxx (擷取完成計費後的餘額)
if(帳戶餘額 < 0) rollback
else commit
20單並發,每單在判斷餘額的時候應該都是足夠的,然後之後寫表操作,第一次扣餘額成功,接下來19單扣餘額失敗,但是你的代碼中沒有任何處理,就導致了建立了訂單,但是沒有扣餘額的情況
解決方案樓上都說了,用事務提交,一開始先update餘額欄位,然後再做餘下操作,這樣能保證並發的時候在餘額這裡有一個鎖,其它請求都要等到這個請求被commit或者rollback以後才能執行
首先,樓主最後貼的代碼還是有問題的。
總的來說,這個,需要用到事務和鎖,同時避免一些坑。
第一,檢查mysql的事務層級,我們要在 可重複讀的 層級下。
第二,確認線上資料庫結構,確保讀寫都使用一個資料庫連接(尤其是讀寫分離的情況下)。
第三,首先開啟事務。
第四,開事務後,第一條就是用select for update查詢出使用者的餘額(避免一致性非鎖定讀)。
第五,進行資金判斷和扣減,注意php計算的話,使用bcmath來處理。
第六,所有資金操作都應該有日誌記錄,所有的資料異常或者代碼錯誤都應該記錄日誌。
第七,業務操作後提交事務。
把賬戶餘額計費放在前面,目前的邏輯執行了,但在計費的過程出錯了而已,如金額欄位不能小於0。放在前面計費的話,可以判斷是否執行成功,否則提示錯誤!
問題出在3,4,5這裡,這種邏輯在出現類似並發的集中請求的時候就會出問題。正確邏輯是
3-update table set 餘額 = 餘額 - 金額 where user_id = ? & 餘額 > 金額,檢查本次修改所影響的行數,如果為0表示根本沒更新,就是餘額已經不足了
4-寫訂單
就沒有5了
原始邏輯的問題就是3查詢的時候餘額確實是足夠的,但是等到第5步扣除餘額的時候就不一定了。
嗯,補充一下,有明說的沒錯,就算修改了邏輯涉及重要資料的地方也最好使用事務。
同上,涉及金錢或者類似的,一定要開啟事務。