如何在现有的 Eloquent 模型上“lockForUpdate()”?

Fla*_*ame 5 innodb locking laravel eloquent

lockForUpdate()sharedLock()是 Laravel Eloquent 中用于设置独占锁或共享锁的函数(文档位于此处)。

然而,我找不到一个好的语法来将其应用到单个已经实例化的 Eloquent 模型上。考虑以下示例代码:

DB::transaction(function() {
    // Find the user with ID = 1.
    $user = User::find(1);
    $user->lockForUpdate()->update([
        'balance' => $user->balance + 1
    ]);

    // ... some more stuff happens here in the transaction
});
Run Code Online (Sandbox Code Playgroud)

上面的代码将无法按预期工作。lockForUpdate()此处返回一个新的查询构建器,它将导致所有用户的余额加一。

我希望该balance属性在此事务期间被读取锁定,这样并行发生的任何其他事务都不会通过计算错误的结果来破坏帐户余额。那么如何确保balance在更新该用户时该属性被锁定呢?$user我知道我可以调用以下函数,但为此创建一个新查询(其中也包含变量)似乎有点违反直觉:

$updated = User::query()->where('id', 1)->lockForUpdate()->update([
    'balance' => $user->balance
]);
Run Code Online (Sandbox Code Playgroud)

注意:我想将->increment()和排除->decrement()在此处的等式之外。我无法使用这些函数,因为我需要 Eloquent 的updating// updated/saving事件saved挂钩才能正确触发(并且在使用这些函数时它们不会被触发)。不过,这是可以预料的,有关更多信息,请参阅https://github.com/laravel/framework/issues/18802#issuecomment-593031090

Fla*_*ame 7

嗯,看来我设法找到了这个问题的快速解决方案。

我认为预期的方法是这样做:

DB::transaction(function() {
    // You can also use `findOrFail(1)` or any other query builder functions here depending on your needs.
    $user = User::lockForUpdate()->find(1);
    $user->update([
        'balance' => $user->balance + 1
    ]);
});
Run Code Online (Sandbox Code Playgroud)

然后,这将生成以下 SQL(摘自 MySQL 通用查询日志):

200524 13:36:04    178 Query    START TRANSACTION
178 Prepare select * from `users` where `users`.`id` = ? limit 1 for update
178 Execute select * from `users` where `users`.`id` = 1 limit 1 for update
178 Close stmt  
178 Prepare update `users` set `balance` = ?, `users`.`updated_at` = ? where `id` = ?
178 Execute update `users` set `balance` = 15, `users`.`updated_at` = '2020-05-24 13:36:04' where `id` = 1
178 Close stmt
QUERY     COMMIT
Run Code Online (Sandbox Code Playgroud)