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"
]);
}
我尝试过的事情:
将 cancelOrder()
方法中的所有内容包装到 DB::transaction({}, 5)
闭包中。 (也试过DB::beginTransaction()
方法)
在 $transaction = Transaction::where('order_id', $orderId)
... 查询上使用 ->lockForUpdate()
。
将 createCancelRefund()
内容包装在 DB::transaction({}, 5)
中,但我认为它是 create()
并没有帮助。
尝试使用缓存,但没有那么快。
查看节流,但似乎无法阻止这种情况(如果我说 2 request/min,重复创建仍然会发生)
防止在 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);
}
我有一个可以给用户退款的取消订单方法。
但是,使用 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"
]);
}
我尝试过的事情:
将
cancelOrder()
方法中的所有内容包装到DB::transaction({}, 5)
闭包中。 (也试过DB::beginTransaction()
方法)在
$transaction = Transaction::where('order_id', $orderId)
... 查询上使用->lockForUpdate()
。将
createCancelRefund()
内容包装在DB::transaction({}, 5)
中,但我认为它是create()
并没有帮助。尝试使用缓存,但没有那么快。
查看节流,但似乎无法阻止这种情况(如果我说 2 request/min,重复创建仍然会发生)
防止在 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);
}