Address Race Condition in sendThankcards using pessimistic locking (lockForUpdate)

This commit is contained in:
antv
2026-07-15 22:35:50 +07:00
parent 53fa37d599
commit c48f7aa23d
+36 -23
View File
@@ -47,36 +47,45 @@ class UserService implements UserServiceInterface
$startOfMonth = Carbon::now()->startOfMonth(); $startOfMonth = Carbon::now()->startOfMonth();
$endOfMonth = Carbon::now()->endOfMonth(); $endOfMonth = Carbon::now()->endOfMonth();
$cardsSentToThisUserThisMonth = Administration::where('msnv', $sender->msnv) DB::transaction(function () use ($sender, $receiverMsnv, $amount, $startOfMonth, $endOfMonth) {
->where('receiver', $receiverMsnv) // Lock sender record to prevent concurrent transaction modifications
->whereBetween('date', [$startOfMonth, $endOfMonth]) $lockedSender = User::where('id', $sender->id)->lockForUpdate()->first();
->sum('sent');
if ($cardsSentToThisUserThisMonth + $amount > Administration::MAX_SEND_CARD_PER_MONTH) { if ($lockedSender->flag_send == config('constants.FLAG_SEND_DISABLED')) {
throw new \App\Exceptions\ThankCardException( throw new \App\Exceptions\ThankCardException(__('messages.error.no_send_permission'));
__('messages.error.max_send_limit_template', [ }
'max' => Administration::MAX_SEND_CARD_PER_MONTH,
'sent' => $cardsSentToThisUserThisMonth
])
);
}
if ($sender->card < $amount) { if ($lockedSender->card < $amount) {
throw new \App\Exceptions\ThankCardException(__('messages.error.not_enough_cards')); throw new \App\Exceptions\ThankCardException(__('messages.error.not_enough_cards'));
} }
// Lock & check total sent cards to the receiver this month
$cardsSentToThisUserThisMonth = Administration::where('msnv', $lockedSender->msnv)
->where('receiver', $receiverMsnv)
->whereBetween('date', [$startOfMonth, $endOfMonth])
->lockForUpdate()
->sum('sent');
if ($cardsSentToThisUserThisMonth + $amount > Administration::MAX_SEND_CARD_PER_MONTH) {
throw new \App\Exceptions\ThankCardException(
__('messages.error.max_send_limit_template', [
'max' => Administration::MAX_SEND_CARD_PER_MONTH,
'sent' => $cardsSentToThisUserThisMonth
])
);
}
DB::transaction(function () use ($sender, $receiverMsnv, $amount) {
Administration::create([ Administration::create([
'msnv' => $receiverMsnv, 'msnv' => $receiverMsnv,
'received' => $amount, 'received' => $amount,
'sender' => $sender->msnv, 'sender' => $lockedSender->msnv,
'sent' => 0, 'sent' => 0,
'receiver' => null, 'receiver' => null,
'date' => Carbon::today(), 'date' => Carbon::today(),
]); ]);
Administration::create([ Administration::create([
'msnv' => $sender->msnv, 'msnv' => $lockedSender->msnv,
'received' => 0, 'received' => 0,
'sender' => null, 'sender' => null,
'sent' => $amount, 'sent' => $amount,
@@ -84,12 +93,16 @@ class UserService implements UserServiceInterface
'date' => Carbon::today(), 'date' => Carbon::today(),
]); ]);
$sender->card -= $amount; $lockedSender->card -= $amount;
if ($sender->card <= 0) { if ($lockedSender->card <= 0) {
$sender->card = 0; $lockedSender->card = 0;
$sender->flag_send = config('constants.FLAG_SEND_DISABLED'); $lockedSender->flag_send = config('constants.FLAG_SEND_DISABLED');
} }
$sender->save(); $lockedSender->save();
// Synchronize variables back to the original model instance
$sender->card = $lockedSender->card;
$sender->flag_send = $lockedSender->flag_send;
}); });
} }