Herokuc92
Herokuc92

Reputation: 3

Laravel Eloquent pessmestic lock to update balance

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

Answers (1)

IGP
IGP

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.

  1. run 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.
  2. On another terminal, run 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

Related Questions