Doctrine 没有通过多个并发请求正确更新实体数据
Doctrine not updating properly entity data with several concurrent requests
我需要做一些基本的事情,我有两个实体:用户和操作。每个用户都有管理员分配的 X 个令牌,然后他可以根据令牌的数量执行 Y 个操作。所以假设一个用户只有足够的令牌来执行一个操作,我确定如果我在同一时间同时执行多个请求(比如同时执行 5 个或更多请求)。用户执行两个或多个操作而不是一个操作(并且仅在解释的场景中,其余一切正常)
我解释的相关代码:
public function useractions(Requests $request){
$user = $this->getUser();
$post = Request::createFromGlobals();
if($post->request->has('new_action') && $this->isCsrfTokenValid("mycsrf", $post->request->get('csrf_token'))) {
$entityManager = $this->getDoctrine()->getManager();
$tokens = $user->getTokens();
if($tokens<1){
$error = "Not enough tokens";
}
if(empty($error)){
$user->setTokens($tokens-1);
$entityManager->flush();
$action = new Action();
$action->setUser($user);
$entityManager->persist($transaction);
$entityManager->flush();
}
}
}
我正在使用 Mariadb 10.5.12 和 InnoDB 作为引擎
显然我在我的代码中犯了一个大错误,或者在 Symfony 或 Doctrine 配置中遗漏了一些东西。有人可以告诉我错误吗?谢谢
您可能在用户的 SELECT
(用户刷新,由您的身份验证机制调用)和您的 UPDATE
(第一个 $entityManager->flush();
)之间存在竞争条件。
Doctrine 有一个内置的方法来管理并发请求,即 EntityManager::transactional()
方法 (doc)。您可能只需要将代码包装到其中:
$entityManager = $this->getDoctrine()->getManager();
$error = $entityManager->transactional(function($entityManager) use ($user) {
// Required to perform a blocking SELECT for UPDATE
$entityManager->refresh($user);
$tokens = $user->getTokens();
if($tokens<1){
return "Not enough tokens";
}
$user->setTokens($tokens-1);
$entityManager->persist($user);
});
if (empty($error)) {
$action = new Action();
$action->setUser($user);
$entityManager->persist($action);
$entityManager->flush();
}
注意:确保你的事务足够快,因为它在 mariadb 端有阻塞效应,影响涉及的行。
对于一个更新命令的执行,您应该使用一次刷新,而不是在每个迭代中。
此外,您可以使用事务将所有这些作为一个单元来完成。
但是如果你想防止并发更新,你应该使用版本控制!这样,当学说想要更新某些内容时,检查版本控制,如果它发生更改,则会抛出错误并防止将数据持久保存到数据库中(您可以通过侦听器处理此错误),并且通过这种方式,只有第一个请求才会成功。
例如,我将 updatedAt filed 标记为一个 version filed,它会在每次持久化时被检查。
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Version
* @ORM\Column(type="datetime",options={"default": "CURRENT_TIMESTAMP"})
*/
private $UpdatedAt;
我需要做一些基本的事情,我有两个实体:用户和操作。每个用户都有管理员分配的 X 个令牌,然后他可以根据令牌的数量执行 Y 个操作。所以假设一个用户只有足够的令牌来执行一个操作,我确定如果我在同一时间同时执行多个请求(比如同时执行 5 个或更多请求)。用户执行两个或多个操作而不是一个操作(并且仅在解释的场景中,其余一切正常)
我解释的相关代码:
public function useractions(Requests $request){
$user = $this->getUser();
$post = Request::createFromGlobals();
if($post->request->has('new_action') && $this->isCsrfTokenValid("mycsrf", $post->request->get('csrf_token'))) {
$entityManager = $this->getDoctrine()->getManager();
$tokens = $user->getTokens();
if($tokens<1){
$error = "Not enough tokens";
}
if(empty($error)){
$user->setTokens($tokens-1);
$entityManager->flush();
$action = new Action();
$action->setUser($user);
$entityManager->persist($transaction);
$entityManager->flush();
}
}
}
我正在使用 Mariadb 10.5.12 和 InnoDB 作为引擎
显然我在我的代码中犯了一个大错误,或者在 Symfony 或 Doctrine 配置中遗漏了一些东西。有人可以告诉我错误吗?谢谢
您可能在用户的 SELECT
(用户刷新,由您的身份验证机制调用)和您的 UPDATE
(第一个 $entityManager->flush();
)之间存在竞争条件。
Doctrine 有一个内置的方法来管理并发请求,即 EntityManager::transactional()
方法 (doc)。您可能只需要将代码包装到其中:
$entityManager = $this->getDoctrine()->getManager();
$error = $entityManager->transactional(function($entityManager) use ($user) {
// Required to perform a blocking SELECT for UPDATE
$entityManager->refresh($user);
$tokens = $user->getTokens();
if($tokens<1){
return "Not enough tokens";
}
$user->setTokens($tokens-1);
$entityManager->persist($user);
});
if (empty($error)) {
$action = new Action();
$action->setUser($user);
$entityManager->persist($action);
$entityManager->flush();
}
注意:确保你的事务足够快,因为它在 mariadb 端有阻塞效应,影响涉及的行。
对于一个更新命令的执行,您应该使用一次刷新,而不是在每个迭代中。
此外,您可以使用事务将所有这些作为一个单元来完成。
但是如果你想防止并发更新,你应该使用版本控制!这样,当学说想要更新某些内容时,检查版本控制,如果它发生更改,则会抛出错误并防止将数据持久保存到数据库中(您可以通过侦听器处理此错误),并且通过这种方式,只有第一个请求才会成功。
例如,我将 updatedAt filed 标记为一个 version filed,它会在每次持久化时被检查。
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Version
* @ORM\Column(type="datetime",options={"default": "CURRENT_TIMESTAMP"})
*/
private $UpdatedAt;