Laravel - 防止多个请求同时创建重复记录

sen*_*nty 3 php deadlock laravel eloquent

我有一个取消订单的方法可以退款给用户。

但是,使用 API,如果用户为同一记录调用端点两次(在循环中),它会向用户退款两次。如果我一次尝试 3 次 Api 调用,前 2 个请求会获得退款,第三个请求不会。

public function cancelOrder($orderId) {
   // First I tried to solve with cache, 
   // but it is not fast enough to catch the loop
   if (Cache::has("api-canceling-$orderId")) {
        return response()->json(['message' => "Already cancelling"], 403);
   }

   Cache::put("api-voiding-$labelId", true, 60);

   // Here I get the transaction, and check if 'transaction->is_cancelled'.
   // I thought cache will be faster but apparently not enough.
   $transaction = Transaction::where('order_id', $orderId)
         ->where('user_id', auth()->user()->id)
         ->where('type', "Order Charge")
         ->firstOrFail();

   if ($transaction->is_cancelled) {
        return response()->json(['message' => "Order already cancelled"], 403);
   }

   // Here I do the api call to 3rd party service and wait for response
   try {
       $result = (new OrderCanceller)->cancel($orderId);
   } catch (Exception $e) {
       return response()->json(['message' => $e->getMessage()], 403);
   }

   $transaction->is_cancelled = true;
   $transaction->save();

   // This is the operation getting called twice.
   Transaction::createCancelRefund($transaction);

   return response()->json(['message' => 'Refund is made to your account']);
}
Run Code Online (Sandbox Code Playgroud)

createCancelRefund()方法如下所示:

public static function createCancelRefund($transaction) {
     Transaction::create([
         'user_id' => $transaction->user_id,
         'credit_movement' => $transaction->credit_movement * -1,
         'type' => "Order Refund"
     ]);
}
Run Code Online (Sandbox Code Playgroud)

我尝试过的事情:

  • cancelOrder()方法中的所有内容包装到DB::transaction({}, 5)闭包中。(也试过DB::beginTransaction()方法)

  • 使用->lockForUpdate()$transaction = Transaction::where('order_id', $orderId)...查询。

  • createCancelRefund()内容包装在里面DB::transaction({}, 5),但我认为create()它没有帮助。

  • 尝试使用缓存,但没有那么快。

  • 查找节流,但似乎不能阻止这种情况(如果我说 2 个请求/分钟,仍然会发生重复创建)

防止内部重复退款创建的正确方法是createCancelRefund()什么?

sen*_*nty 6

原子锁解决了我的问题。

原子锁允许操作分布式锁而无需担心竞争条件。例如,Laravel Forge 使用原子锁来确保一次在服务器上只执行一个远程任务

public function cancelOrder($orderId) {
   return Cache::lock("api-canceling-{$orderId}")->get(function () use ($orderId) {
      $transaction = Transaction::where('order_id', $orderId)
            ->where('user_id', auth()->user()->id)
            ->where('type', "Order Charge")
            ->firstOrFail();

      if ($transaction->is_cancelled) {
           return response()->json(['message' => "Order already cancelled"], 403);
      }

      try {
         $result = (new OrderCanceller)->cancel($orderId);
      } catch (Exception $e) {
       return response()->json(['message' => $e->getMessage()], 403);
      }

      $transaction->is_cancelled = true;
      $transaction->save();

      // This is the operation getting called twice.
      Transaction::createCancelRefund($transaction);

      return response()->json(['message' => 'Refund is made to your account']);
   });
   
   return response()->json(['message' => "Already cancelling"], 403);
}
Run Code Online (Sandbox Code Playgroud)