Laravel取消提现接口循环调用时increment方法重复执行问题
解决取消提现接口并发请求导致余额重复累加的问题
我开发了一个处理用户取消提现请求的功能,流程如下:
- 检查用户是否存在该提现请求
- 删除该提现请求
- 将提现金额返还至用户余额
但通过AJAX循环调用该接口时,出现了余额被重复累加的问题。
原PHP代码
public function cancel_withdraw(Request $request){ $validated = $request->validate([ 'withdraw_id' => 'required|numeric' ]); $withdraw=Withdraw::where('user_id',Auth::id()) ->where('id',$request->withdraw_id) ->whereNull('status') ->first(); $withdraw->delete(); Wallet::where('user_id',Auth::id())->increment($withdraw->coin,$withdraw->amount); return response()->json(['message'=>'Successfully Deleted!','errors' => ['']]); }
原JS代码
$(document).ready(function() { let i = 0; while (i < 100) { $.ajax({ url: "{{ url('cancel_withdraw') }}", type:'POST', dataType: "json", data: "_token={{ csrf_token() }}&withdraw_id="+withdraw_id, success: function(data) {}, error: function(data){} }); e.preventDefault(); i++; } });
问题原因
- 并发竞态冲突:循环发起的100次请求几乎同时到达服务器,第一个请求还没执行删除操作时,其他请求已经查询到了这条未处理的提现记录,导致多次触发余额累加。
- 操作非原子性:查询、删除、余额更新三步是独立执行的,没有加锁或事务控制,无法保证操作的唯一性。
- 接口无幂等性:同一个取消请求被多次执行时,会产生重复的副作用(余额重复增加)。
解决方案
方案一:用状态标记+事务控制原子操作
不直接删除提现记录,先标记为已取消,再执行余额累加,同时用数据库事务保证操作的原子性,避免并发冲突:
public function cancel_withdraw(Request $request){ $validated = $request->validate([ 'withdraw_id' => 'required|numeric' ]); DB::beginTransaction(); try { // 先更新状态锁定记录,避免并发查询 $updated = Withdraw::where('user_id', Auth::id()) ->where('id', $request->withdraw_id) ->whereNull('status') ->update(['status' => 'cancelled']); if (!$updated) { DB::rollBack(); return response()->json(['message' => '提现请求不存在或已处理', 'errors' => ['']]); } // 获取更新后的提现记录 $withdraw = Withdraw::find($request->withdraw_id); // 累加余额 Wallet::where('user_id', Auth::id())->increment($withdraw->coin, $withdraw->amount); DB::commit(); return response()->json(['message'=>'取消提现成功!','errors' => ['']]); } catch (\Exception $e) { DB::rollBack(); return response()->json(['message'=>'操作失败','errors' => [$e->getMessage()]]); } }
方案二:用悲观锁锁定提现记录
查询时给记录加行级锁,确保同一时间只有一个请求能处理这条记录:
public function cancel_withdraw(Request $request){ $validated = $request->validate([ 'withdraw_id' => 'required|numeric' ]); DB::beginTransaction(); try { // 加行级锁,防止并发请求同时读取记录 $withdraw = Withdraw::where('user_id', Auth::id()) ->where('id', $request->withdraw_id) ->whereNull('status') ->lockForUpdate() ->first(); if (!$withdraw) { DB::rollBack(); return response()->json(['message' => '提现请求不存在或已处理', 'errors' => ['']]); } $withdraw->status = 'cancelled'; $withdraw->save(); // 若一定要删除,可替换为 $withdraw->delete(); 但建议保留记录方便对账 Wallet::where('user_id', Auth::id())->increment($withdraw->coin, $withdraw->amount); DB::commit(); return response()->json(['message'=>'取消提现成功!','errors' => ['']]); } catch (\Exception $e) { DB::rollBack(); return response()->json(['message'=>'操作失败','errors' => [$e->getMessage()]]); } }
前端优化:避免重复请求
前端循环发起100次请求本身不符合业务逻辑,应该限制用户只能发起一次请求:
$(document).ready(function() { let isProcessing = false; // 假设是点击按钮触发取消操作 $('#cancelWithdrawBtn').on('click', function(e) { e.preventDefault(); // 正在处理时禁止再次发起请求 if (isProcessing) return; isProcessing = true; $.ajax({ url: "{{ url('cancel_withdraw') }}", type:'POST', dataType: "json", data: { _token: "{{ csrf_token() }}", withdraw_id: withdraw_id }, success: function(data) { // 处理成功逻辑,比如提示用户或刷新页面 }, error: function(data){ // 处理错误逻辑,比如提示用户操作失败 }, complete: function() { // 请求完成后解锁 isProcessing = false; } }); }); });
内容的提问来源于stack exchange,提问作者Arlong
相关产品推荐
相关产品推荐

