当两个用户同时投票给自己时,学说僵局
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);
}
我在使用 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);
}