Reputation: 3
I have a table that contains balances and they are deducted when a user purchases an item, I'm trying to lock to make sure the balance is valid and deducted properly. I have a for loop that takes multiple balances and decreases them one by one in a request. What I've done is wrap my code with a DB:transaction as shown below, but this seems wrong as the for loop might take longer than expected, thus every other user trying to update this balance (!!which is shared for many users that can edit simultaneously!!)
$balances->map(function ($balance) {
// Check if balance is valid
if (!Balances::where('id', $balance->id)->deductBalance($balance->balance)) {
return response()->json(['message' => 'Insufficient '], 400);
};
DB:create([
....
]);
});
deductBalance:
public function deductBalance($balance) {
$this->balance-= $balance;
if ($this->balance >= 0) {
$this->save();
return true;
}
return false;
}
I added this as shown in the documentation
DB::transaction(function () {
// Check if balance is valid
if (!Balances::where('id', $balance->id)->deductBalance($balance->balance)) {
return response()->json(['message' => 'Insufficient '], 400);
};
}, 5);
Imagine 5 users try and update 5 balances at the same exact time, is the solution above efficient to prevent the balance from being a negative value? I can already see a problem in this for loop, if one balance is invalid, the request will be rejected, but all previous balances are deducted, should I have another for loop within the transaction to check all balances first and then update them? Thanks.
Upvotes: 0
Views: 2971
Reputation: 15786
DB::transaction()
doesn't lock anything. It just makes sure to roll back the every query inside the transaction if one of them fails.
What you should do if you want to lock a resource is the following:
DB::transaction(function () use ($reserva, $validated) {
// lock
$balance = Balances::lockForUpdate()->find($balance->id) // or where->(...)->get() if it's more than 1 balance.
...
});
lockForUpdate()
inside a transaction prevents any query from affecting the resources selected until the transaction is over.
In this case, update/delete queries that have an effect on the Balance
with id = 1 will be put on hold. OTHER balances (id != 1) can be updated and are not locked.
EDIT
Here is an easy way to see database locks in action.
php artisan tinker
on one terminal and run the following code
DB::transaction(function () {
SomeTestModel::lockForUpdate()->find(1)->update(['attribute' => 'value']);
sleep(60); // artificially increase how long the transaction is going to take by 60 seconds.
});
Do not close the terminal.php artisan tinker
and run the following code:
// Will run instantly
SomeTestModel::find(2)->update(['attribute' => 'value']);
// Will not run until transaction on previous terminal is done. Might even throw a lock wait timeout, showing the resource is properly locked.
SomeTestModel::find(1)->update(['attribute' => 'value']);
Upvotes: 3