Laravel 11应用层如何防止健身会话的竞态条件问题
解决Laravel中创建健身会话的竞态条件问题
问题背景
我有一张名为gym_sessions的表,包含user_id和status字段;status取值对应:1=>活跃、2=>完成、3=>拒绝。业务规则如下:
- 用户必须有足够余额才能开启新会话
- 用户不能同时拥有两个活跃会话
- 用户可拥有多个状态为2或3的会话
当前实现的创建新会话方法存在竞态条件:当用户并行发送多个请求时,系统会创建多个状态为1的活跃会话。需要在**应用层(使用Laravel方法,不依赖数据库层)**解决该问题,环境为Laravel 11、MySQL、PHP 8.2.8。
原实现代码
// Start new gym session. public function startGymSession(Request $request){ $validator = Validator::make($request->all(),[ 'gym_id' => ['required', 'exists:gyms,id'], 'payment_method_id' => ['required', 'exists:payment_methods,id'], 'gym_pricing_id' => ['required', Rule::exists('gym_pricings', 'id')->where(function ($query) use ($request) { $query->where('gym_id', $request->input('gym_id')); }), ], ]); if($validator->fails()){ return $this->sendError('Validation error!'); } $user = Auth::user(); // Check if the user have enough balance to start a gym session. $userbalance = $user->wallet_balance; $gymPricing = GymPricing::find($request->gym_pricing_id)->first(); if($userbalance < $gymPricing->price){ return $this->sendError('Insufficient balance.'); } // Check if the user have active gym sessions. // If the user have active gym sessions, he can't start a new one. // Prevent race condition. /** @var \App\Models\User $user */ if($user->gymSessions()->where('status', 1)->count() > 0){ return $this->sendError('You have an active gym session.'); } $now = Carbon::now(); // Start the gym session. $gymSession = GymSession::create([ 'user_id' => $user->id, 'gym_id' => $request->gym_id, 'payment_method_id' => $request->payment_method_id, 'gym_pricing_id' => $request->gym_pricing_id, 'entry_time' => $now, ]); if($gymSession){ return $this->sendResponse([], 'Gym session started.'); } return $this->sendError('Failed to start gym session.'); }
应用层解决方法(Laravel实现)
1. 使用缓存锁(Cache Lock)
利用Laravel的缓存锁机制,为每个用户的会话创建操作加锁,确保同一时间只有一个请求能执行核心逻辑。锁的有效期需设置合理,避免死锁。
修改后的核心代码:
// 验证通过后添加锁逻辑 $lockKey = "gym_session_lock_{$user->id}"; // 尝试获取锁:有效期10秒,等待2秒后放弃 $lock = Cache::lock($lockKey, 10); if (!$lock->get()) { return $this->sendError('请稍后再试,正在处理您的请求'); } try { // 余额检查 $userbalance = $user->wallet_balance; $gymPricing = GymPricing::find($request->gym_pricing_id)->first(); if($userbalance < $gymPricing->price){ return $this->sendError('Insufficient balance.'); } // 检查活跃会话 if($user->gymSessions()->where('status', 1)->count() > 0){ return $this->sendError('You have an active gym session.'); } // 创建会话 $now = Carbon::now(); $gymSession = GymSession::create([ 'user_id' => $user->id, 'gym_id' => $request->gym_id, 'payment_method_id' => $request->payment_method_id, 'gym_pricing_id' => $request->gym_pricing_id, 'entry_time' => $now, ]); return $gymSession ? $this->sendResponse([], 'Gym session started.') : $this->sendError('Failed to start gym session.'); } finally { // 无论成功失败都释放锁 $lock->release(); }
2. 使用数据库事务+用户记录悲观锁
通过对用户记录加悲观锁,确保请求处理期间,其他请求无法操作该用户的相关数据或执行相同逻辑。
修改后的核心代码:
// 验证通过后开启事务并锁定用户记录 DB::beginTransaction(); try { // 锁定用户记录,防止并发操作 $user = Auth::user()->lockForUpdate(); // 余额检查(使用锁定后的用户对象) $userbalance = $user->wallet_balance; $gymPricing = GymPricing::find($request->gym_pricing_id)->first(); if($userbalance < $gymPricing->price){ DB::rollBack(); return $this->sendError('Insufficient balance.'); } // 检查活跃会话 if($user->gymSessions()->where('status', 1)->count() > 0){ DB::rollBack(); return $this->sendError('You have an active gym session.'); } // 创建会话 $now = Carbon::now(); $gymSession = GymSession::create([ 'user_id' => $user->id, 'gym_id' => $request->gym_id, 'payment_method_id' => $request->payment_method_id, 'gym_pricing_id' => $request->gym_pricing_id, 'entry_time' => $now, ]); if($gymSession){ DB::commit(); return $this->sendResponse([], 'Gym session started.'); } DB::rollBack(); return $this->sendError('Failed to start gym session.'); } catch (\Exception $e) { DB::rollBack(); return $this->sendError('Request failed: ' . $e->getMessage()); }
3. 使用队列(Queue)串行处理请求
将创建会话的逻辑放入队列,同一用户的请求会被串行处理,从根源上避免竞态条件。
控制器修改示例
public function startGymSession(Request $request){ $validator = Validator::make($request->all(),[ 'gym_id' => ['required', 'exists:gyms,id'], 'payment_method_id' => ['required', 'exists:payment_methods,id'], 'gym_pricing_id' => ['required', Rule::exists('gym_pricings', 'id')->where(function ($query) use ($request) { $query->where('gym_id', $request->input('gym_id')); }), ], ]); if($validator->fails()){ return $this->sendError('Validation error!'); } // 将任务推入队列 StartGymSessionJob::dispatch(Auth::user()->id, $request->all()); return $this->sendResponse([], '请求已受理,正在为您创建会话'); }
队列任务示例
class StartGymSessionJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $userId; protected $requestData; public function __construct(int $userId, array $requestData) { $this->userId = $userId; $this->requestData = $requestData; } public function handle() { $user = User::findOrFail($this->userId); // 余额检查 $userbalance = $user->wallet_balance; $gymPricing = GymPricing::find($this->requestData['gym_pricing_id'])->first(); if($userbalance < $gymPricing->price){ // 可添加用户通知逻辑 return; } // 检查活跃会话 if($user->gymSessions()->where('status', 1)->count() > 0){ // 可添加用户通知逻辑 return; } // 创建会话 $now = Carbon::now(); GymSession::create([ 'user_id' => $user->id, 'gym_id' => $this->requestData['gym_id'], 'payment_method_id' => $this->requestData['payment_method_id'], 'gym_pricing_id' => $this->requestData['gym_pricing_id'], 'entry_time' => $now, ]); // 可添加用户通知逻辑 } }
内容的提问来源于stack exchange,提问作者Ahmed Laggoun
相关产品推荐
相关产品推荐

