当两个用户同时投票给自己时,学说僵局

Doctrine deadlock when two users vote themselves concurrently

我在使用 Symfony 3.4 应用程序时遇到问题,用户可以对其他用户的条目进行投票。它使用 FOSRestBundle 作为 API,使用 Doctrine 来实现数据持久化。代码很简单:

public function voteEntryAction(Request $request, ChallengeEntry $challengeEntry)
{
    /** @var User */
    $user = $this->getUser();
    $em   = $this->getDoctrine()->getManager();

    $challengeVote = $em->getRepository(ChallengeVote::class)->findOneBy([
        'user'           => $user,
        'challengeEntry' => $challengeEntry,
    ]);

    if ($challengeVote) {
        throw new BadRequestHttpException('User has already voted for this challenge entry.');
    }

    $challengeVote = new ChallengeVote();
    $challengeVote
        ->setUser($user)
        ->setChallengeEntry($challengeEntry)
    ;

    $form = $this->createForm(ChallengeVoteType::class, $challengeVote);
    $form->submit($request->request->all());

    if ($form->isSubmitted() && $form->isValid()) {
        $em->persist($challengeVote);

        // updates the statistics
        $user->addGivenChallengeVote($challengeVote); // <=== here and the next line are the problematic lines

        $challengeEntry->getUser()->addReceivedChallengeVote($challengeVote); // <=== here and the previous line are the problematic lines

        // SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction
        $em->flush();

        return $user;
    }

    return $this->view($data, Response::HTTP_BAD_REQUEST);
}

有时我会收到此错误:

SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction

我能够重现该错误,它发生在 当 2 个用户同时投票给自己时。这是因为每个用户都在更新另一个用户。我该如何解决这个问题?

我尝试了以下选项:

尝试的解决方案 1

我在其他用户的更新之前添加了另一个 $em->flush。不是最优但似乎有效:

// updates the statistics
$user->addGivenChallengeVote($challengeVote);

$em->flush();

// adds a different flush for the other user in order to avoid the following error:
// SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction
$challengeEntry->getUser()->addReceivedChallengeVote($challengeVote);

$em->flush();

尝试的解决方案 2

我已经尝试捕获 RetryableException 并再次进行冲洗,但我收到错误 The EntityManager is closed

尝试的解决方案 3

我尝试用 $em->resetManager() 重置 EntityManager,但随后所有实体都被分离,甚至用户也被视为新用户。

你在正确的轨道上,但你错过了一个关键要素。重置实体管理器后,您需要再次 select 您的实体,无论如何这样更好。

这是一篇很棒的博客 post,其中包含示例:link

最后 double flush 解决方案并不可靠,所以我不得不 重置实体管理器 并再次检索所有实体。

问题出在方法签名中的 当前用户 ChallengeEntry 对象。我不得不保存他们的 ID 并在死锁后再次检索它们。

这个解决方案不是很漂亮,但看起来很可靠。如果您有更好的想法或解决方案请告诉我。

我还为带有特殊错误代码的移动应用程序提供了回退,以便可以完全重试请求。

完整代码如下:

public function voteEntryAction(Request $request, ChallengeEntry $challengeEntry)
{
    $retryCount = 3;
    $deadlock   = false;

    /** @var User */
    $user   = $this->getUser();
    $userId = $user->getId();

    $challengeEntryId = $challengeEntry->getId();

    for ($i = 0; $i < $retryCount; ++$i) {
        try {
            $em = $this->getEntityManager();

            // gets the user and the challenge entry after the deadlock
            if ($deadlock) {
                /** @var User */
                $user = $em->getRepository(User::class)->find($userId);

                /** @var ChallengeEntry */
                $challengeEntry = $em->getRepository(ChallengeEntry::class)->find($challengeEntryId);
            }

            return $this->voteEntry($em, $user, $challengeEntry, $request);
        } catch (RetryableException $e) {
            $deadlock = true;

            usleep(200000); // 200 milliseconds
        }
    }

    $data = new ErrorApiResponse(ErrorApiResponse::DEADLOCK_ERROR, 'A concurrency error occurred, please try again later.');

    return $this->view($data, Response::HTTP_SERVICE_UNAVAILABLE);
}


private function getEntityManager()
{
    /** @var EntityManager */
    $em = $this->getDoctrine()->getManager();

    // resets the entity manager
    if (!$em->isOpen()) {
        $this->getDoctrine()->resetManager();
        $em = $this->getDoctrine()->getManager();
    }

    return $em;
}

private function voteEntry(ObjectManager $em, User $user, ChallengeEntry $challengeEntry, Request $request)
{
    $count = $em->getRepository(ChallengeVote::class)->countByUserAndChallengeEntry($user, $challengeEntry);

    if ($count > 0) {
        throw new BadRequestHttpException('User has already voted for this challenge entry.');
    }

    $challengeVote = new ChallengeVote();
    $challengeVote
        ->setUser($user)
        ->setChallengeEntry($challengeEntry)
    ;

    $form = $this->createForm(ChallengeVoteType::class, $challengeVote);
    $form->submit($request->request->all());

    if ($form->isSubmitted() && $form->isValid()) {
        $em->persist($challengeVote);

        $event = new ChallengeEntryVotedEvent($challengeVote);
        $this->dispatcher->dispatch(ChallengeEntryVotedEvent::NAME, $event);

        // updates the statistics
        $user->addGivenChallengeVote($challengeVote);

        $challengeEntry->getUser()->addReceivedChallengeVote($challengeVote);

        $em->flush();

        return $user;
    }

    $data = $this->formErrorHelper->convertFormToErrorApiResponse($form);

    return $this->view($data, Response::HTTP_BAD_REQUEST);
}