You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 21:46:15