You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

高并发请求场景下Doctrine无法正确更新Entity实体数据问题咨询

问题根因

你遇到的是典型的并发竞态条件问题,底层逻辑如下:
多个并发请求几乎同时读取到同一个用户的token剩余值(此时剩余值为1),所有请求都通过了$tokens<1的校验,之后各自执行$tokens-1的扣减逻辑,最终就会出现token被多次扣除、超出可用次数的异常。


现有代码的具体问题
  • 未做并发控制:读取用户token值、校验、扣减的整个流程没有加锁,也没有用原子更新,多个请求可以同时拿到相同的旧token值做计算
  • 隐式事务拆分:你两次调用flush()是两个独立的隐式事务,token扣减和Action记录的写入没有原子性保障
  • 代码笔误:你创建的Action实例是$action,但persist()方法传入的是未定义的$transaction,运行时会抛出变量不存在的错误

修复方案

你可以根据业务并发量选以下任意一种方案,基于你的InnoDB引擎都可以稳定生效:

方案1:悲观锁(适合并发量较高的场景)

给用户行加排他锁,同一时间只有一个请求可以操作该用户的token数据,操作逻辑包在同一个显式事务中:

use Doctrine\DBAL\LockMode;

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();
        // 开启显式事务
        $entityManager->beginTransaction();
        try {
            // 给用户行加排他写锁,其他事务要修改该用户数据必须等待当前事务提交
            $entityManager->lock($user, LockMode::PESSIMISTIC_WRITE);
            // 加锁后重新读取最新的token值
            $tokens = $user->getTokens();
            $error = '';
            if($tokens<1){
                $error = "Not enough tokens";
            }
            if(empty($error)){
                $user->setTokens($tokens-1);
                $action = new Action();
                $action->setUser($user);
                $entityManager->persist($action);
                // 一次性提交所有修改
                $entityManager->flush();
                $entityManager->commit();
            } else {
                $entityManager->rollback();
                // 这里返回错误提示
            }
        } catch (\Exception $e) {
            $entityManager->rollback();
            throw $e;
        }
     }
}

方案2:原子更新(性能最优,无需显式加锁)

直接用数据库原子更新语句做扣减,依托InnoDB的行锁机制保证只有一个请求能扣减成功,无需额外加锁:

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();
        $connection = $entityManager->getConnection();
        // 原子扣减,只有token>=1的时候才会更新成功,返回影响行数为1
        $affectedRows = $connection->executeStatement(
            'UPDATE user SET tokens = tokens - 1 WHERE id = ? AND tokens >= 1',
            [$user->getId()]
        );
        if ($affectedRows === 0) {
            $error = "Not enough tokens";
            // 返回错误
        } else {
            // 扣减成功后再写入Action记录
            $action = new Action();
            $action->setUser($user);
            // 同步刷新用户实体的token值,避免后续逻辑用旧值
            $user->setTokens($user->getTokens() -1);
            $entityManager->persist($action);
            $entityManager->flush();
        }
     }
}

方案3:乐观锁(适合并发量较低的场景)

给User实体新增一个version字段,标注为Doctrine的乐观锁版本字段,每次更新用户数据时会自动校验版本号,并发冲突时会抛出异常,你捕获后重试即可,无需手动加锁。

你需要先给User实体加如下注解:

/**
 * @ORM\Column(type="integer")
 * @ORM\Version
 */
private $version;

之后正常做token扣减、flush即可,并发冲突时Doctrine会自动抛出OptimisticLockException,你捕获后返回操作频繁提示或者自动重试即可。


内容的提问来源于stack exchange,提问作者Actum1968

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 17:54:03