PHP Doctrine悲观写锁无法阻止并发请求读取旧库存数据
环境配置
- PHP: 8.1
- Doctrine ORM: 2.9.2
问题描述
库存管理应用中,多请求同时更新库存时出现并发问题:
- 初始库存:50
- 第一个请求:将库存50+5,最终得到55
- 第二个请求:等待第一个请求完成后,仍使用初始库存50+5,得到55(预期应为55+5=60)
锁机制已生效(第二个请求会等待第一个完成),但无法读取前一个请求更新后的库存值。
持久化代码
$this->em->beginTransaction(); try{ $inventoryID = $stockAdjustment->getInventory()->getId(); $this->detach($stockAdjustment->getInventory()); $inventory = $this->inventoryRepo->find($inventoryID, LockMode::PESSIMISTIC_WRITE); $this->refresh($inventory); sleep(15); $stockAdjustment->setInventory($inventory); $stockAdjustment->setPreAdjustmentStock($inventory->getStock()); $inventory->modifyStock($stockAdjustment->getQuantity()); $this->em->persist($inventory); $this->em->persist($stockAdjustment); $this->em->flush(); $this->em->commit(); } catch (\Throwable $e){ $this->em->rollback(); throw $e; }
注:操作前库存是从调整对象水合时获取的,保留了detach和refresh代码以展示已尝试的操作;原本Inventory与InventoryAdjustment配置为级联持久化,本次已移除该配置,改为直接保存库存。
问题分析与解决方案
核心问题
代码中多余的refresh调用是关键问题:当第一个事务持有行锁时,第二个事务在find阶段等待锁释放;第一个事务提交后,第二个事务的find本应获取最新的库存数据,但后续的refresh会重新从数据库加载数据——结合默认的数据库事务隔离级别(如REPEATABLE READ),会导致同一个事务内重复读取到旧的数据快照。
另外,detach操作并非必要,反而可能引入缓存一致性问题。
修正后的代码
$this->em->beginTransaction(); try{ $inventoryID = $stockAdjustment->getInventory()->getId(); // 直接通过ID加悲观写锁获取最新库存,绕过EntityManager缓存 $inventory = $this->inventoryRepo->find($inventoryID, LockMode::PESSIMISTIC_WRITE); sleep(15); $stockAdjustment->setInventory($inventory); $stockAdjustment->setPreAdjustmentStock($inventory->getStock()); $inventory->modifyStock($stockAdjustment->getQuantity()); $this->em->persist($inventory); $this->em->persist($stockAdjustment); $this->em->flush(); $this->em->commit(); } catch (\Throwable $e){ $this->em->rollback(); throw $e; }
额外注意事项
- 事务隔离级别:确保数据库使用的隔离级别支持悲观锁的预期行为(如MySQL InnoDB默认的REPEATABLE READ即可,若需更严格一致性可考虑SERIALIZABLE,但会降低并发性能)。
- modifyStock方法校验:确认
modifyStock方法是对库存值进行累加操作(如$this->stock += $quantity),而非直接赋值覆盖。 - 避免提前加载实体:在进入并发更新的事务前,不要提前加载Inventory实体,防止EntityManager缓存旧数据干扰锁后的读取。
内容的提问来源于stack exchange,提问作者D.Jones
相关产品推荐
相关产品推荐

