EntityManager::lock()方法的实际适用场景是什么?
我在Symfony 5.4 + Doctrine环境下开发,需要实现Reseller实体余额的并发安全更新(给现有余额加10),避免并发请求导致脏数据。
控制器通过URL参数自动加载实体:
#[Route('/{id}/add10ToBalance', name: 'cp_partners_resellers_edit')] public function add10ToBalance(Request $request, Reseller $reseller): Response
之后执行事务操作:
$this->entityManager->getConnection()->beginTransaction(); // 暂停自动提交 try { $this->entityManager->lock($reseller, LockMode::PESSIMISTIC_WRITE); $reseller->setBalance($reseller->getBalance() + 10); $this->entityManager->persist($reseller); $this->entityManager->flush(); $this->entityManager->getConnection()->commit(); } catch (\Exception $e) { $this->entityManager->getConnection()->rollBack(); throw $e; }
通过SQL日志发现执行序列是:
SELECT * FROM reseller WHERE id = ? START TRANSACTION SELECT 1 FROM reseller t0 WHERE t0.id = ? FOR UPDATE UPDATE reseller SET balance = ? WHERE id = ? COMMIT
这里的问题很明显:lock()方法不会从数据库刷新实体数据。如果在第一次SELECT和START TRANSACTION之间,该实体被其他请求修改,那么当前进程内的实体数据已经过期,即使数据库被锁定,更新的还是旧值。
我找到的可行解决方案是用带锁的find()重新获取实体:
$reseller = $this->entityManager->find(Reseller::class, $reseller->getId(), LockMode::PESSIMISTIC_WRITE);
对应的正确SQL序列:
SELECT * FROM reseller WHERE id = ? START TRANSACTION SELECT * FROM reseller WHERE id = ? FOR UPDATE UPDATE reseller SET balance = ? WHERE id = ? COMMIT
这让我产生疑问:既然EntityManager::lock()可能导致锁定过期数据,那它的实际用途到底是什么?
解答:EntityManager::lock()的适用场景
lock()方法并非设计用来替代「加锁+刷新数据」的操作,它的核心作用是为EntityManager中已存在的实体添加数据库级别的锁,适用场景包括:
短事务内的即时锁定:如果在同一个请求/事务中,你刚通过find()、createQuery等方式获取了实体,且能确保从获取实体到调用lock()的这段时间里,没有外部进程会修改该实体,此时用lock()可以直接对现有实体加锁,无需重新查询。比如:
$reseller = $this->entityManager->find(Reseller::class, $id); // 此处没有外部修改的可能 $this->entityManager->lock($reseller, LockMode::PESSIMISTIC_WRITE); // 执行更新操作配合乐观锁使用:当使用
LockMode::OPTIMISTIC或LockMode::OPTIMISTIC_FORCE_INCREMENT时,lock()会检查实体的版本号(需实体配置@Version注解),如果数据库中的版本和当前实体版本不一致,会直接抛出OptimisticLockException,此时不需要刷新数据,而是做版本验证。长事务中的延迟加锁:在一些长事务场景中,你可能先加载实体做一系列业务校验,直到最后需要修改时才加锁。这种情况下如果担心数据过期,可以先调用
$entityManager->refresh($reseller)刷新实体到最新状态,再调用lock()加锁,确保基于最新数据进行操作:$this->entityManager->beginTransaction(); try { // 先加载实体做业务校验 $reseller = $this->entityManager->find(Reseller::class, $id); // 执行一系列非修改操作... // 准备修改前,刷新并加锁 $this->entityManager->refresh($reseller); $this->entityManager->lock($reseller, LockMode::PESSIMISTIC_WRITE); $reseller->setBalance($reseller->getBalance() + 10); $this->entityManager->flush(); $this->entityManager->commit(); } catch (\Exception $e) { $this->entityManager->rollBack(); throw $e; }
简单来说,lock()是给EntityManager管理的现有实体添加锁的工具,它默认不会主动刷新数据——因为它假设你已经确认当前实体数据是可用的。如果无法保证数据新鲜度,就需要手动刷新或用带锁的find()重新获取。
内容的提问来源于stack exchange,提问作者michnovka

