【问题标题】:How to lock database for Laravel's `firstOrCreate`?如何为 Laravel `firstOrCreate` 锁定数据库?
【发布时间】:2020-03-17 08:43:35
【问题描述】:

我们目前在执行以下代码时遇到Duplicate entryQueryException:

Slug::firstOrCreate([
  Slug::ENTITY_TYPE => $this->getEntityType(),
  Slug::SLUG        => $slug
], [
  Slug::ENTITY_ID   => $this->getKey()
]);

由于 Laravel 的 firstOrCreate 方法在插入之前首先检查具有属性的条目是否存在,因此永远不会发生此异常。但是,我们有一个应用程序,每天有数百万的访问者和数百万的操作,因此还使用带有两个从属的主数据库连接进行读取。因此,可能会出现一些竞争条件。

我们目前尝试分离查询并强制主连接读取:

 $slugModel = Slug::onWriteConnection()->where([
     Slug::SLUG        => $slug,
     Slug::ENTITY_TYPE => $this->getEntityType()
 ])->first();

 if ($slugModel && $slugModel->entity_id !== $this->getKey()) {
    $class = get_class($this);
    throw new \RuntimeException("The slug [{$slug}] already exists for a model of type [{$class}].");
}

if (!$slugModel) {
   return $this->slugs()->create([
         Slug::SLUG        => $slug,
         Slug::ENTITY_TYPE => $this->getEntityType()
   ]);
}

但有时仍会发生异常。

我们的下一个方法是在读取检查之前锁定表并在写入之后释放锁定,以防止在我们的读取和写入之间从其他数据库操作中插入具有相同 slug 的任何插入。有谁知道如何解决这个问题?我真的不明白Laravel's Pessimistic Locking 如何帮助解决问题。我们使用 MySql 作为我们的数据库。

【问题讨论】:

  • 我认为悲观锁定不能解决你的问题。这些类型的锁定用于锁定从多个连接更新的资源。此外,由于您的流量非常高,我不建议在没有适当研究它对系统的影响的情况下使用表锁定。 pro and cons of locking tableoptimistic-vs-pessimistic-locking
  • 我自己还在学习数据库并发问题,但是通过阅读“设计数据密集型应用程序”,您遇到的现象称为“写入倾斜”。作者说:“自动防止写入倾斜需要真正的可序列化隔离。”在锁定或设置新的隔离级别之前,IMO 可能值得考虑您的应用程序如何处理重复项。如果 slug 是自动生成的,您可以处理异常并生成一个新的。如果它们是用户定义的,用户可以在不同的数据库表或内存数据库中“预先保留” slug。
  • 感谢您的回复!我们将尝试处理异常并为这些情况生成一个新异常。谢谢!

标签: php mysql laravel eloquent locking


【解决方案1】:

我不建议锁定表格,尤其是在您有数百万观众的情况下。

大多数竞争条件都可以通过锁来解决,但这不能用锁来解决,因为你不能锁定不存在的行(有类似间隙锁定的东西,但这在这里没有帮助。)。

Laravel 不自己处理竞争条件。如果您调用 firstOrCreate 它会执行两个查询:

  1. 选择 slug=X 和 entity_type=Y 的项目
  2. 如果不存在,则创建它

现在因为我们有两个查询,竞争条件是可能的,这意味着两个用户并行到达步骤 1,然后都尝试在步骤 2 中创建条目,您的系统将崩溃。

由于您已经有 Duplicate Key 错误,这意味着您已经在标识您的行的两列上的元组上设置了唯一约束,这很好。

你现在可以做的是to catch the duplicate key error,像这样:

try{
    $slug = Slug::firstOrCreate([
  Slug::ENTITY_TYPE => $this->getEntityType(),
  Slug::SLUG        => $slug
], [
  Slug::ENTITY_ID   => $this->getKey()
]);
}
catch (Illuminate\Database\QueryException $e){
    $errorCode = $e->errorInfo[1];
    if($errorCode == 1062){
        $slug = Slug::where('slug','=', $slug)->where('entity_type','=',  $this->getEntityType())->first();
    }
}

【讨论】:

【解决方案2】:

对此的一种解决方案是使用 Laravel 队列并确保它一次运行一个作业,这样您将永远不会同时有 2 个相同的查询。

如果您想在同一个请求中返回结果,这肯定行不通。

【讨论】:

    猜你喜欢
    • 2018-01-14
    • 2016-05-09
    • 2023-04-08
    • 1970-01-01
    • 2021-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多