Home > database >  When to use transaction in laravel
When to use transaction in laravel

Time:09-15

I am currently making a turn based strategy game with laravel (mysql DB with InnoDB) engine and want to make sure that I don't have bugs due to race conditions, duplicate requests, bad actors etc...

Because these kind of bugs are hard to test, I wanted to get some clarification.

Many actions in the game can only occur once per turn, like buying a new unit. Here is a simplified bit of code for purchasing a unit.

$player = Player::find($player_id);
if($player->gold >= $unit_price && $player->has_purchased == false){
    $player->has_purchased = true;
    $player->gold -= $unit_price;
    $player->save();
    
    $unit = new Unit();
    $unit->player_id = $player->id;
    $unit->save();
}

So my concern would be if two threads both made it pass the if statement and then executed the block of code at the same time.

  1. Is this a valid concern?

  2. And would the solution be to wrap everything in a database transaction like https://betterprogramming.pub/using-database-transactions-in-laravel-8b62cd2f06a5 ?

This means that a good portion of my code will be wrapped around database transactions because I have a lot of instances that are variations of the above code for different actions.

Also there is a situation where multiple users will be able to update a value in the database so I want to avoid a situation where 2 users increment the value at the same time and it only gets incremented once.

CodePudding user response:

Since you are using Laravel to presumably develop a web-based game, you can expect multiple concurrent connections to occur. A transaction is just one part of the equation. Transactions ensure operations are performed atomically, in your case it ensures that both the player and unit save are successful or both fail together, so you won't have the situation where the money is deducted but the unit is not granted.

However there is another facet to this, if there is a real possibility you have two separate requests for the same player coming in concurrently then you may also encounter a race condition. This is because a transaction is not a lock so two transactions can happen at the same time. The implication of this is (in your case) two checks happen on the same player instance to ensure enough gold is available, both succeed, and both deduct the same gold, however two distinct units are granted at the end (i.e. item duplication). To avoid this you'd use a lock to prevent other threads from obtaining the same player row/model, so your full code would be:

$player = Player::where('id',$player_id)->lockForUpdate()->first();
DB::transaction(function () use ($player, $unit_price) {
    if($player->gold >= $unit_price && $player->has_purchased == false){
        $player->has_purchased = true;
        $player->gold -= $unit_price;
        $player->save();
    
        $unit = new Unit();
        $unit->player_id = $player->id;
        $unit->save();
    }
});

This will ensure any other threads trying to retrieve the same player will need to wait until the lock is released (which will happen at the end of the first request).

There's more nuances to deal with here as well like a player sending a duplicate request from double-clicking for example, and that can get a bit more complex.

CodePudding user response:

For you purchase system, it's advisable to implement DB:transaction since it protects you from false records. Checkout the laravel docs for more information on this https://laravel.com/docs/9.x/database#database-transactions
As for reactive data you need to keep track of, simply bind a variable to that data in your frontEnd, then use the variable to update your DB records.

CodePudding user response:

In the case you need to exit if any exception or error occurs. If an exception is thrown the data will not save and rollback all the transactions. I recommand to use transactions as possible as you can. The basic format is:

DB::beginTransaction();

try {
    // database actions like create, update etc.

    DB::commit(); // finally commit to database
} catch (\Exception $e) {
    DB::rollback(); // roll back if any error occurs
    // something went wrong
}

See the laravel docs here

  • Related