49
loading...
This website collects cookies to deliver better user experience
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $fillable = [
'name',
'balance',
];
protected $casts = [
'balance' => 'float',
];
}
class Bank
{
public static function sendMoney(User $from, User $to, float $amount)
{
if ($from->balance < $amount) {
return false;
}
$from->update(['balance' => $from->balance - $amount]);
$to->update(['balance' => $to->balance + $amount]);
return true;
}
}
$alice = User::find(1); // 'balance' => 100
$bob = User::find(2); // 'balance' => 0
Bank::sendMoney($alice, $bob, 100); // true
$from->update(['balance' => $from->balance - $amount]);
$to->update(['balance' => $to->balance + $amount]);
use Illuminate\Support\Facades\DB;
class Bank
{
public static function sendMoney(User $from, User $to, float $amount)
{
if ($from->balance < $amount) {
return false;
}
return DB::transaction(function () {
$from->update(['balance' => $from->balance - $amount]);
$to->update(['balance' => $to->balance + $amount]);
return true;
});
}
}
// FIRST REQUEST
$alice = User::find(1); // 'balance' => 100,
$bob = User::find(2); // 'balance' => 0,
Bank::sendMoney($alice, $bob, 100); // true
// SECOND REQUEST
$alice = User::find(1); // 'balance' => 100,
$charlie = User::find(3); // 'balance' => 0,
Bank::sendMoney($alice, $charlie, 100); // true, but should have been false
find()
) at the same time, both requests will read that Alice has $100 in her account, which can be false because if the other transaction has already changed the balance, we remain with a reading saying she still has $100.Request1: Reads Alice balance as $100
Request2: Reads Alice balance as $100
Request1: Subtract $100 from Alice
Request2: Subtract $100 from Alice
Request1: Add $100 to Bob
Request2: Add $100 to Charlie
Request1: Reads Alice balance as $100
Request1: Subtract $100 from Alice.
Request1: Add $100 to Bob
Request2: Reads Alice balance as $0
Request2: Don't allow Alice to send money
A “for update” lock prevents the selected records from being modified or from being selected with another shared lock.
lockForUpdate
in our find()
statements, they will not be selected by another shared lock.A shared lock prevents the selected rows from being modified until your transaction is committed.
find()
queries, the rows (in the first one Alice & Bob, in the second one Alice & Charlie) will not be read, nor modified until our update
transaction got committed successfully.// FIRST REQUEST
DB::transaction(function () {
$alice = User::lockForUpdate()->find(1); // 'balance' => 100
$bob = User::lockForUpdate()->find(2); // 'balance' => 0
Bank::sendMoney($alice, $bob, 100); // true
});
// SECOND REQUEST
DB::transaction(function () {
$alice = User::lockForUpdate()->find(1); // 'balance' => 0
$charlie = User::lockForUpdate()->find(3); // 'balance' => 0
Bank::sendMoney($alice, $charlie, 100); // false
});
lockForUpdate
would be just enough, because, by definition, any rows selected by it will never be selected by another shared lock, either lockForUpdate()
or sharedLock()
.sharedLock()
just so other queries won’t select the same rows until the transaction is finished. The use case would be for strong read consistency, making sure that if another transaction may be in process, to not get outdated rows.