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

Laravel - Prevent multiple requests at the same time creating duplicate records

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

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

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']);
}

createCancelRefund() 方法如下所示:

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

我尝试过的事情:

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

Atomic Lock 解决了我的问题。

Atomic locks allow for the manipulation of distributed locks without worrying about race conditions. For example, Laravel Forge uses atomic locks to ensure that only one remote task is being executed on a server at a time

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);
}