如何在数据库中按优先级分批扣减库存(基于订单顺序与可用量)

如何在数据库中按优先级分批扣减库存(基于订单顺序与可用量)

本文介绍一种安全、可控的库存扣减策略:当用户下单数量超过单条库存记录余量时,按订单顺序(如 id_order)遍历多条同商品库存记录,逐条扣减直至满足需求,避免超卖或负库存。

在电商或酒类仓储系统中,同一商品(如 id_wine = 1)可能因来自不同采购/入库订单而分散存储于多条库存记录中(每条对应独立 id_order)。此时若用户一次性下单 5 瓶,而数据库中存在两条记录:qty=4(order 1)和 qty=1(order 2),理想行为应是先扣减第一条 4 瓶,再扣减第二条 1 瓶,而非仅操作第一条导致余量变为 -1——这正是原子性不足与逻辑缺失引发的典型超卖风险。

直接使用 ->decrement() 单次操作无法实现“跨行智能分配”,因此需采用显式事务化分步处理。核心思路是:

  1. 预校验总可用量(防止无库存下单);
  2. 按业务优先级排序获取库存记录(如 orderBy(‘id_order’) 或 orderBy(‘created_at’));
  3. 循环扣减,动态更新待扣数量,遇余量不足则跳转至下一条;
  4. 全程包裹数据库事务,确保全部成功或全部回滚。

以下是推荐的 Laravel 实现(含事务与边界防护):

问小白

问小白

免费使用DeepSeek满血版

下载

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),便于对账与审计。

该方案兼顾数据一致性、业务可解释性与扩展性,是处理分布式库存扣减的稳健实践。

https://www.php.cn/faq/2011889.html

发表回复

Your email address will not be published. Required fields are marked *