使用 Symfony 5.4 和 Doctrine,我有一个需要在考虑并发安全的情况下更新的实体。如果同时有 2 个请求,我需要向现有余额添加余额并避免过时的数据。
这是我的控制器:
#[Route('/{id}/add10ToBalance', name: 'cp_partners_resellers_edit')]
public function add10ToBalance(Request $request, Reseller $reseller): Response
正如您所看到的
Reseller
实体是从 URL 加载的。
既然我有了实体,我就:
$this->entityManager->getConnection()->beginTransaction(); // suspend auto-commit
try {
$this->entityManager->lock($reseller, LockMode::PESSIMISTIC_WRITE);
$reseller->setBalance($reseller->getBalance() + 10);
$this->entityManager->persist($reseller);
$this->entityManager->flush();
$this->entityManager->getConnection()->commit();
我从调试器中的 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
之间,如果实体被修改,我现在正在数据库中使用锁定版本,但我的 Symfony 进程中的数据已过时。
显而易见的解决方案是使用
$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()
方法会导致锁定潜在的过时数据,那么它的真正用途是什么?
想象一下逻辑情况(它存在),您必须锁定一个实体,但数据修改是在其他一个或多个实体上完成的。
例如,B 实体的列表连接到一个 A 实体。您想为给定的 A 创建一个新的 B 实体,但无论是否允许,它都有复杂的业务逻辑。并且条件取决于给定 A 实体的 B 实体的现有列表。 您要做的操作如下。
那么,在读取B列表和插入新B之间,任何人都不应该做同样的操作,因为如果在检查条件和写入新B的过程中出现了新B,那么检查结果就会失效。
那么你可以对什么进行锁定呢?您必须在给定的 A 实体上执行此操作,即使您实际上没有修改它。您只需插入一个新的 B 项目。而且这里你需要一个悲观锁。
这是 EntityManager::lock 方法不会导致潜在的陈旧数据的情况。