首页 > 解决方案 > Laravel Eloquent 悲观锁更新余额

问题描述

我有一个包含余额的表,当用户购买商品时会扣除余额,我试图锁定以确保余额有效并正确扣除。我有一个 for 循环,它需要多个余额并在请求中一一减少它们。我所做的是用 DB:transaction 包装我的代码,如下所示,但这似乎是错误的,因为 for 循环可能需要比预期更长的时间,因此每个其他用户都试图更新这个余额(!!这是为许多用户共享的可以同时编辑!!)

$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([
        ....
    ]);
});

扣除余额:

public function deductBalance($balance) {
    $this->balance-= $balance;
    if ($this->balance >= 0) {
        $this->save();
        return true;
    }
    return false;
}

我添加了这个,如文档中所示

DB::transaction(function () {
    // Check if balance is valid
    if (!Balances::where('id', $balance->id)->deductBalance($balance->balance)) {
        return response()->json(['message' => 'Insufficient '], 400);
    };
}, 5);

想象一下 5 个用户同时尝试更新 5 个余额,上述解决方案是否有效地防止余额为负值?我已经可以看到这个for循环的一个问题,如果一个余额无效,请求将被拒绝,但所有以前的余额都会被扣除,我是否应该在事务中再有一个for循环先检查所有余额然后更新它们?谢谢。

标签: phplaraveleloquent

解决方案


DB::transaction()不锁定任何东西。如果其中一个失败,它只是确保回滚事务中的每个查询。

如果要锁定资源,您应该执行以下操作:

DB::transaction(function () use ($reserva, $validated) {
   // lock
   $balance = Balances::lockForUpdate()->find($balance->id) // or where->(...)->get() if it's more than 1 balance.
   ...
});

lockForUpdate()在事务内部防止任何查询影响选定的资源,直到事务结束。

在这种情况下,对 id = 1 有影响的更新/删除查询Balance将被搁置。其他余额 (id != 1) 可以更新且未锁定。

编辑

这是查看数据库锁的一种简单方法。

  1. 在一个终端上运行php artisan tinker并运行以下代码
    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.
    });
    
    不要关闭终端。
  2. 另一个终端上,运行php artisan tinker并运行以下代码:
    // 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']);
    

推荐阅读