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

Posted

技术标签:

【中文标题】使用数据库事务防止竞争条件 (Laravel)【英文标题】:Preventing Race Conditions Using Database Transactions (Laravel) 【发布时间】:2017-07-24 10:23:39 【问题描述】:

如何防止发生这种竞争情况?我知道 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();

秒后,我运行请求 2,它只是将一些内容连接到用户名列并保存:

public function raceCondition2() 

    DB::beginTransaction();

    $user = User::findorfail(7);

    sleep(6);

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

    $user->save();

    DB::commit();

本例在数据库中的结果是:USER_

在第一个请求之前从数据库读取的第二个请求可以保存,并且使用了陈旧的 NULL 值。有没有办法在另一个事务正在进行时锁定数据库以防止读取? (即让第二个请求等待第一个请求完成?)

【问题讨论】:

【参考方案1】:

Laravel 支持“悲观锁定”。有关这方面的更多信息,请参阅Laravel documentation on pessimistic locking。

【讨论】:

【参考方案2】:

当您执行查询并希望有一种机制在执行过程中可能出现错误时反转结果修改时使用事务。

您正在寻找的是内部锁定方法,其中对数据库的每个请求都放入队列中,并且仅在处理前一个请求时才处理。

我不知道这个功能是否来自 laravel ORM 的开箱即用,但它可以通过经典的 sql 查询轻松实现。

查看此链接,了解整个机制的工作原理,我相信您正在寻找的是行级别锁定。

Locking Methods

【讨论】:

【参考方案3】:

要解决应用程序的竞速条件问题需要高性能,乐观锁定优于悲观锁定,因为悲观锁定可能会产生死锁。

事实上,乐观锁并不是一个数据库特性,它只是一种最佳实践。

有关更多详细信息,您可以查看这个很棒的答案:Optimistic locking in mysql

【讨论】:

【参考方案4】:

我在做什么是因为我遇到了很多错误

production.ERROR: SQLSTATE[23000]: 完整性约束违规:1062 Duplicate entry

这是基于使用队列的多线程表插入...是多用途多线程多人......等等......这对我来说很简单。至少到目前为止它似乎有效

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();
           
       
    

【讨论】:

以上是关于使用数据库事务防止竞争条件 (Laravel)的主要内容,如果未能解决你的问题,请参考以下文章

Java MySQL 防止竞争条件

使用拆分“读取”和“写入”数据库连接时 Laravel 中的竞争条件

处理事务数据库表中的竞争条件

当用户连续执行多个graphql突变时防止竞争条件

多个 NSManagedObjectContexts - 防止竞争条件和死锁

当发生写入数据库的并发 API 调用(或服务器速度较慢时)时,防止出现竞争条件