
本文介绍一种安全、可控的库存扣减策略:当用户下单数量超过单条库存记录余量时,按订单顺序(如 id_order)遍历多条同商品库存记录,逐条扣减直至满足需求,避免超卖或负库存。
在电商或酒类仓储系统中,同一商品(如 id_wine = 1)可能因来自不同采购/入库订单而分散存储于多条库存记录中(每条对应独立 id_order)。此时若用户一次性下单 5 瓶,而数据库中存在两条记录:qty=4(order 1)和 qty=1(order 2),理想行为应是先扣减第一条 4 瓶,再扣减第二条 1 瓶,而非仅操作第一条导致余量变为 -1——这正是原子性不足与逻辑缺失引发的典型超卖风险。
直接使用 ->decrement() 单次操作无法实现“跨行智能分配”,因此需采用显式事务化分步处理。核心思路是:
- 预校验总可用量(防止无库存下单);
- 按业务优先级排序获取库存记录(如 orderBy('id_order') 或 orderBy('created_at'));
- 循环扣减,动态更新待扣数量,遇余量不足则跳转至下一条;
- 全程包裹数据库事务,确保全部成功或全部回滚。
以下是推荐的 Laravel 实现(含事务与边界防护):
use Illuminate\Support\Facades\DB;
$requestedQty = $request->quantita;
$wineId = $wine_id;
$restaurantId = Auth::user()->id_restaurant;
// ✅ 步骤1:预检查总可用库存(防幻读,建议加锁或使用 SELECT FOR UPDATE)
$totalAvailable = warehouse::where('id_restaurant', $restaurantId)
->where('id_wine', $wineId)
->where('quantita_restante', '>', 0)
->sum('quantita_restante');
if ($totalAvailable < $requestedQty) {
throw new \Exception("库存不足:仅剩 {$totalAvailable} 瓶,请求 {$requestedQty} 瓶");
}
// ✅ 步骤2:开启事务,按订单顺序获取可扣减记录
DB::transaction(function () use ($restaurantId, $wineId, $requestedQty) {
// 加锁确保并发安全(MySQL/PostgreSQL 支持)
$stocks = warehouse::where('id_restaurant', $restaurantId)
->where('id_wine', $wineId)
->where('quantita_restante', '>', 0)
->orderBy('id_order') // 或 orderBy('created_at') 依业务规则
->lockForUpdate() // 关键:防止并发修改
->get();
$remaining = $requestedQty;
foreach ($stocks as $stock) {
if ($remaining <= 0) break;
$take = min($stock->quantita_restante, $remaining);
// 执行原子扣减(等价于 UPDATE ... SET quantita_restante = quantita_restante - ?)
$stock->decrement('quantita_restante', $take);
$remaining -= $take;
}
if ($remaining > 0) {
// 理论上不会触发(因已预校验 totalAvailable),但作为兜底
throw new \Exception("库存状态异常:仍有 {$remaining} 瓶未扣减成功");
}
});⚠️ 关键注意事项:
- 永远不要跳过预校验:仅靠循环扣减无法阻止并发场景下其他请求抢先扣光库存;
- 必须使用 lockForUpdate()(或等效行锁),否则高并发时仍可能产生负库存;
- 若业务要求“优先消耗最早入库批次”,请改用 orderBy('created_at') 替代 id_order;
- 对于超大库存集,可考虑分页处理或引入 Redis 预减缓存提升性能;
- 生产环境建议记录扣减日志(warehouse_id, before, after, by_order_id),便于对账与审计。
该方案兼顾数据一致性、业务可解释性与扩展性,是处理分布式库存扣减的稳健实践。










