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;