Laravel `increment()` 是否锁定了行?

Does the Laravel `increment()` lock the row?

在 Eloquent 模型上调用 Laravel increment() 是否锁定行?

例如:

 $userPoints = UsersPoints::where('user_id','=',\Auth::id())->first();
 if(isset($userPoints)) {
      $userPoints->increment('points', 5);
 }

如果在竞争条件下从两个不同的位置调用它,第二个调用是否会覆盖第一个增量并且我们仍然只得到 5 分?或者他们会加起来,我们最终得到 10 分吗?

回答这个问题(对以后的读者有帮助):你问的问题取决于数据库配置。

大多数 MySQL 引擎:MyISAM 和 InnoDB 等。在插入、更新或更改 table 时使用锁定,直到明确关闭此功能。 (无论如何,在大多数情况下,这是唯一正确且可以理解的实现方式)

因此您可以对所得到的感到满意table,因为它可以在任意数量的并发调用中正常工作:

-- this is something like what laravel query builder translates to
UPDATE users SET points += 5 WHERE user_id = 1

并以零为起始值调用两次,最终将达到 10

对于 Laravel 中 ->increment() 的具体情况,答案实际上略有不同:

如果调用 $user->increment('credits', 1),将执行以下查询:

UPDATE `users`
SET `credits` = `credits` + 1
WHERE `id` = 2

这意味着查询可以被视为 atomic,因为实际的 credits 金额是在查询中检索的,而不是使用单独的 [=17] 检索的=].

因此您可以在没有 运行 任何 DB::transaction() 包装器或 lockForUpdate() 调用的情况下执行此查询,因为它总是会正确地增加它。

为了显示可能出错的地方,BAD 查询将如下所示:

# Assume this retrieves "5" as the amount of credits:
SELECT `credits` FROM `users` WHERE `id` = 2;

# Now, execute the UPDATE statement separately:
UPDATE `users`
SET `credits` = 5 + 1, `users`.`updated_at` = '2022-04-15 23:54:52'
WHERE `id` = 2;

或 Laravel 等价物(不要这样做):

$user = User::find(2);
// $user->credits will be 5.
$user->update([
    // Shown as "5 + 1" in the query above but it would be just "6" ofcourse.
    'credits' => $user->credits + 1
]);

现在,这很容易出错,因为您是 'assigning' 信用值,这取决于 SELECT 语句发生的时间。因此 2 个查询可以将信用更新为相同的值,而意图是将其递增两次。但是,您可以通过以下方式更正此 Laravel 代码:

DB::transaction(function() {
    $user = User::query()->lockForUpdate()->find(2);
    $user->update([
        'credits' => $user->credits + 1,
    ]);
});

现在,由于 2 个查询包含在一个事务中并且 ID 为 2 的用户记录是 READ-locked 使用 lockForUpdate(),任何第二个(或第三个或 n-th)实例在锁定事务完成之前,不应使用 SELECT 查询读取此并行发生的事务。