Doctrine 异常:尝试获取锁时发现死锁。

4
我有一个Symfony应用程序,它公开了一组JSON Web服务,供移动应用程序使用。
在过去的几天里,我们有很多并发用户使用该应用程序(每天约5000次访问),并且我的日志中开始“随机”出现Doctrine错误。它每天大约出现2-3次,并且这是错误信息:
Uncaught PHP Exception Doctrine\DBAL\Exception\DriverException: "An exception occurred while executing 'UPDATE fos_user_user SET current_crystals = ?, max_crystals = ?, updated_at = ? WHERE id = ?' with params [31, 34, "2017-12-19 09:31:18", 807]:

SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction" at /var/www/html/rollinz_cms/releases/98/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php line 115

在更新用户表时似乎无法获取锁定。控制器代码如下:

/**
 * @Rest\Post("/api/badges/{id}/achieve", name="api_achieve_badge")
 */
public function achieveAction(Badge $badge = null)
{
    if (!$badge) {
        throw new NotFoundHttpException('Badge not found.');
    }

    $user      = $this->getUser();
    $em        = $this->getDoctrine()->getManager();
    $userBadge = $em->getRepository('AppBundle:UserBadge')->findBy(array(
        'user'  => $user,
        'badge' => $badge,
    ));

    if ($userBadge) {
        throw new BadRequestHttpException('Badge already achieved.');
    }

    $userBadge = new UserBadge();
    $userBadge
        ->setUser($user)
        ->setBadge($badge)
        ->setAchievedAt(new \DateTime())
    ;
    $em->persist($userBadge);

    // sets the rewards
    $user->addCrystals($badge->getCrystals());

    $em->flush();

    return new ApiResponse(ApiResponse::STATUS_SUCCESS, array(
        'current_crystals'    => $user->getCurrentCrystals(),
        'max_crystals'        => $user->getMaxCrystals(),
    ));
}

我查阅了MySQL和Doctrine文档,但没有找到可靠的解决方案。Doctrine建议重试事务,但没有给出实际例子:

https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlock-example.html

try {
    // process stuff
} catch (\Doctrine\DBAL\Exception\RetryableException $e) {
    // retry the processing
}

这篇文章建议重试事务,我该怎么做?

可能是服务器问题(访问过多),我必须提升服务器性能,还是代码有问题,我必须在代码中明确处理死锁?

3个回答

3
这是一个MySQL问题。多个并发事务阻塞相同的资源。
请检查是否有可能会长时间阻塞记录的cronjobs。
否则,只是并发请求更新相同的数据,您可能更清楚在哪里更新这些数据。
以下是在php中尝试重试的粗略方法:
$retry=0;
while (true) {
    try {
      // some more code
      $em->flush();
      return new ApiResponse(ApiResponse::STATUS_SUCCESS, array(
         'current_crystals'    => $user->getCurrentCrystals(),
         'max_crystals'        => $user->getMaxCrystals(),
      ));
    } catch (DriverException $e) {
       $retry++;
       if($retry>3) { throw $e; }
       sleep(1); //optional
    }
}

你的解决方案看起来不错,但是我找不到RetryableException类。我甚至更新了我的doctrine/orm版本。我需要安装特定的composer包吗? - StockBreak
看起来RetryableException需要DBAL 2.6,但不幸的是这需要PHP 7.1,而我只能使用7.0。我可以在没有RetryableException的情况下使用以前的代码,并将其替换为另一个异常吗?谢谢! - StockBreak
实际上,我发布的代码可以使用任何类型的异常来完成,并且它应该在任何版本的PHP >= 5.3中工作。将异常更改为您拥有的DriverException,并检查它是否具有代码1213。如果是,请使用该代码再试一次。 - albert
现在我遇到了这个异常 The EntityManager is closed,我需要在 flush 之前调用 $this->getDoctrine()->resetManager();$em = $this->getDoctrine()->getManager(); 吗? - StockBreak

2
Albert的解决方案是正确的,但你还必须在catch子句中使用ManagerRegistry的resetManager()方法重新创建一个新的EntityManager。如果继续使用旧的EntityManager,你将会得到异常,并且其行为将不可预测。还要注意对旧的EntityManager的引用。
希望这个问题能够在Doctrine 3中得到修正:查看问题
在此期间,我建议采用自定义EntityManager来解决这个问题。

0
在@albert的基础上(谢谢)
  1. 在文件顶部添加以下代码:use Doctrine\DBAL\Exception\DeadlockException;
  2. 将此函数添加到一个类中(例如:Service)
    public function retryflush($maxretry = 3)
    {
        $em = $this->doc->getManager();

        $retry=0;
        while ($retry >= 0) {
            try {
                $em->flush();
                $retry = -1;
            } catch (DeadlockException $e) {
                $retry++;
                if($retry>$maxretry) { throw $e; }
                sleep(513); //optional
            }
        }
        return $retry;
    }

重新定义
$em = $this->doc->getManager(); // use your own entitymanager.

3. 像这样运行它
$classname->retryflush(5) // retry 5 times

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接