基于FIFO的库存出库:如何批量扣减多行库存数量
库存FIFO出库多行扣减的正确处理方案
你的现有代码逻辑本身可以处理多行库存扣减的场景(比如10+5+5=20的出库需求),但存在事务缺失、并发安全、无库存校验等关键问题,以下是优化后的完整解决方案:
现有代码的执行流程(对应你的场景)
针对卖出20件、库存为10/5/5的情况,你的代码会按以下步骤执行:
- 读取第一条10件的库存:
$quantity[$i](20)≥10,将该库存行设为0,剩余待扣减数量变为10 - 读取第二条5件的库存:剩余数量10≥5,将该库存行设为0,剩余待扣减数量变为5
- 读取第三条5件的库存:剩余数量5≥5,将该库存行设为0,剩余待扣减数量变为0,跳出循环
这个流程逻辑是对的,但一旦中间步骤出错(比如数据库异常),会导致部分库存已扣减、部分未处理的不一致状态,同时无法应对并发下单的超卖问题。
优化后的完整代码
// 开启数据库事务,确保所有出库操作原子性 DB::beginTransaction(); try { foreach ($product_id as $index => $productId) { $remainingQty = $quantity[$index]; if ($remainingQty <= 0) continue; // 加悲观锁锁定库存行,防止并发修改导致超卖 $stockEntries = DB::table('purchase_products') ->select('id', 'quantity', 'purchase_price') ->where('product_id', $productId) ->where('quantity', '>', 0) ->orderBy('created_at', 'asc') ->lockForUpdate() ->get(); // 提前校验总库存是否足够 $totalAvailable = $stockEntries->sum('quantity'); if ($totalAvailable < $remainingQty) { throw new Exception("商品ID {$productId} 库存不足,当前仅剩余 {$totalAvailable} 件"); } foreach ($stockEntries as $entry) { if ($remainingQty <= 0) break; // 计算当前行需要扣减的数量 $deductQty = min($remainingQty, $entry->quantity); if ($deductQty === $entry->quantity) { // 扣完当前库存行 DB::table('purchase_products') ->where('id', $entry->id) ->update(['quantity' => 0]); } else { // 扣减部分库存 DB::table('purchase_products') ->where('id', $entry->id) ->update(['quantity' => $entry->quantity - $deductQty]); } $remainingQty -= $deductQty; } } // 所有操作完成后提交事务 DB::commit(); return redirect('sales')->with('success', '出库操作已完成'); } catch (Exception $e) { // 出错时回滚所有操作 DB::rollBack(); return redirect('sales')->with('error', $e->getMessage()); }
关键优化说明
- 事务包裹:所有库存扣减操作在事务中执行,中途任何错误都会回滚,避免数据不一致
- 悲观锁控制并发:使用
lockForUpdate()锁定查询到的库存行,防止多个请求同时修改同一份库存导致超卖 - 提前库存校验:先计算总可用库存,不足时直接抛出错误,避免无效循环
- 简化逻辑:用
min()函数直接计算扣减数量,减少冗余的分支判断 - 变量可读性优化:用
remainingQty替代原数组索引访问,代码更清晰
内容的提问来源于stack exchange,提问作者Hola
相关产品推荐
相关产品推荐

