我们当前在执行以下代码时遇到
Duplicate entry
QueryException:
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 的悲观锁定 如何帮助解决这个问题。我们使用 MySql 作为我们的数据库。
我不建议锁定桌子,特别是如果你有数百万观众。
大多数竞争条件可以通过锁来修复,但这不能通过锁来修复,因为你无法锁定不存在的行(有类似间隙锁定的东西,但这在这里没有帮助。)。
Laravel 本身不处理竞争条件。如果您调用firstOrCreate,它会执行两个查询:
现在因为我们有两个查询,竞争条件是可能的,这意味着两个用户并行到达步骤 1,然后都尝试在步骤 2 中创建条目,并且您的系统将崩溃。
由于您已经遇到了
Duplicate Key
错误,这意味着您已经对标识行的两列上的元组施加了唯一的约束,这很好。
您现在可以做的是捕获重复键错误,如下所示:
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();
}
}
解决此问题的一个解决方案是使用 Laravel 队列并确保它一次运行一项作业,这样您就永远不会同时有 2 个相同的查询。
如果你想在同一个请求中返回结果,这肯定行不通。
这个问题从Laravel 10.22.0开始已经彻底解决,请通过翻译阅读:[Laravel] createOrFirstの登场から激変したfirstOrCreate,updateOrCreateに迫る!
如果在唯一键约束下重试处理还不够:mpyw/laravel-database-advisory-lock:Laravel 上 Postgres/MySQL/MariaDB 的咨询锁定功能