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

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;
}

额外注意事项

  1. 事务隔离级别:确保数据库使用的隔离级别支持悲观锁的预期行为(如MySQL InnoDB默认的REPEATABLE READ即可,若需更严格一致性可考虑SERIALIZABLE,但会降低并发性能)。
  2. modifyStock方法校验:确认modifyStock方法是对库存值进行累加操作(如$this->stock += $quantity),而非直接赋值覆盖。
  3. 避免提前加载实体:在进入并发更新的事务前,不要提前加载Inventory实体,防止EntityManager缓存旧数据干扰锁后的读取。

内容的提问来源于stack exchange,提问作者D.Jones

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 19:53:08