使用数据库事务防止争用条件(Laravel)

dkqlctbz  于 2023-01-06  发布在  其他
关注(0)|答案(5)|浏览(158)

如何防止这种争用情况发生?我知道Laravel中的事务阻塞更新,但如何防止使用过时数据?是否有方法在另一个事务进行时锁定数据库以防止读取?(即,让第二个请求等待第一个请求完成?)

    • 假设数据库中主键id = 7的用户名字段为空。**

请求1进入并执行以下操作:

public function raceCondition1() {

    DB::beginTransaction();

    //Get the model with primary key 7
    $user = User::findorfail(7);

    sleep(6);

    $user->username = 'MyUsername';

    $user->save();

    DB::commit();
}
    • 两**秒后,我运行Request 2,它只是将一些内容连接到username列并保存:
public function raceCondition2() {

    DB::beginTransaction();

    $user = User::findorfail(7);

    sleep(6);

    $user->username = 'USER_' . $user->username;

    $user->save();

    DB::commit();
}

在这种情况下,数据库中的结果为:用户_
第二个请求在第一个请求可以保存之前从数据库中读取,并使用了过时的NULL值。是否有方法在另一个事务正在进行时锁定数据库以防止读取?(即,让第二个请求等待第一个请求完成?)

yqlxgs2m

yqlxgs2m1#

Laravel支持“悲观锁定”。有关这方面的更多信息,请参考Laravel关于悲观锁定的文档。

xzlaal3s

xzlaal3s2#

当您正在执行查询并且希望拥有一种机制,以便在查询执行过程中可能出错的情况下撤消所产生的修改时,可以使用事务。
正如您正在寻找的是内部锁定方法,其中对数据库的每个请求都放在队列中,只有在处理前一个请求时才进行处理。
我不知道这个特性是否来自laravel ORM,但它可以很容易地从经典的sql查询中实现。
查看这个链接,了解整个机制是如何工作的,我相信您正在寻找的是行级锁定。
Locking Methods

5tmbdcev

5tmbdcev3#

为了解决应用程序的竞争条件问题,需要高性能,乐观锁优于悲观锁,因为悲观锁可能会产生死锁。
实际上,乐观锁不是数据库的特性,它只是一种最佳实践。
要了解更多详细信息,您可以查看这个伟大的答案:Optimistic locking in MySQL

enxuqcxy

enxuqcxy4#

我在做什么因为我得到了一大堆错误
production.ERROR:数据库状态[23000]:完整性约束冲突:1062重复输入
这是基于使用队列的多线程表插入...我认为在Laravel firstOrCreate中这样做是可以的,然后我尝试了updateOrCreate,我必须说这是一个巨大的疏忽,考虑到所有东西都是多线程多人使用的...等等...这对我来说很简单。至少到目前为止它看起来是有效的

public function firstOrCreate(array $attributes, array $values = []) {
     if (! is_null($instance = $this->where($attributes)->first())) {
          return $instance;
     }
     try {
         return tap($this->newModelInstance($attributes + $values), function ($instance) {$instance->save();});
    } catch (Exception $e) {
        return $this->where($attributes)->first();
    }
}

现在,如果竞态条件很活泼,有多个冲突,这仍然会失败,但可以通过使函数循环冗余,或只是使它冗余几次来进行某种对冲...看起来很糟糕,但我没有遇到更多的冲突...

public function firstOrCreate(array $attributes, array $values = []) {
    if (! is_null($instance = $this->where($attributes)->first())) {
        return $instance;
    }
    try {
        return tap($this->newModelInstance($attributes + $values), function ($instance) {$instance->save();});
    } catch (Exception $e) {
            if (! is_null($instance = $this->where($attributes)->first())) {
                return $instance;
            }
            try {
                return tap($this->newModelInstance($attributes + $values), function ($instance) {$instance->save();});
           } catch (Exception $e) {
                return $this->where($attributes)->first();
           }
       }
    }
hrirmatl

hrirmatl5#

对于数字,可以使用increment()、decrement()方法。
示例:$transaction->increment('amount', $amount);
我用简单的代码测试了这个。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

class Transaction extends Model
{
    use HasFactory;

    public static function deposit(int $id, int $amount) {
        DB::beginTransaction();
        $transaction = Transaction::find($id);
        if($transaction) {
            echo "$transaction->id has $transaction->amount \n";
            $transaction->increment('amount', $amount);
            // $transaction->amount += $amount;
            sleep(10);
            $transaction->save();
            echo "After deposit now $transaction->id has $transaction->amount \n";
        }
        DB::commit();
    }

    public static function withdraw(int $id, int $amount) {
        DB::beginTransaction();
        $transaction = Transaction::find($id);
        if($transaction && $transaction->amount >= $amount) {
            echo "$transaction->id has $transaction->amount \n";
            $transaction->decrement('amount', $amount);
            // $transaction->amount -= $amount;
            sleep(10);
            $transaction->save();
            echo "After withdraw now $transaction->id has $transaction->amount \n";
        }
        DB::commit();
    }

}

要测试这个,打开两个不同的终端和tinker,创建一个id =〉1和amount =〉5000的记录。
运行

Transaction::withdraw(1, 500);

在一个端子中,在另一个端子的下方

Transaction::deposit(1, 1000);

通过运行Transaction::all();进行检查

相关问题