电商库存防止超卖机制

设计思路

我们有一个电商系统,库存防止超卖是一个核心问题。从代码仓库中,我们看到了几个关键点:

  1. 在生成订单时,会先检查库存(hasStock方法),然后锁定库存(lockStock方法)。
  2. 锁定库存是通过更新数据库中的锁定库存字段(lock_stock)来实现的,同时会检查库存是否足够。
  3. 支付成功后,会扣减真实库存(reduceSkuStock方法),同时减少锁定库存。
  4. 如果订单取消或超时,会释放锁定库存(releaseSkuStockLock或releaseStockBySkuId方法)。

防止超卖的关键在于:

  • 在用户下单时,先锁定库存,防止其他用户同时购买同一商品导致超卖。
  • 锁定库存时,使用数据库的更新操作,并检查库存是否足够,利用数据库的事务和行锁来保证并发安全。
  • 支付成功后,再扣减真实库存,如果支付失败或取消订单,则释放锁定库存。

具体代码实现:

  1. 在生成订单前,检查库存(OmsPortalOrderServiceImpl.generateOrder方法中):

    // 判断购物车中商品是否都有库存
    if (!hasStock(cartPromotionItemList)) {
        Asserts.fail("库存不足,无法下单");
    }
    
  2. 锁定库存(OmsPortalOrderServiceImpl.lockStock方法):

    private void lockStock(List<CartPromotionItem> cartPromotionItemList) {
        for (CartPromotionItem cartPromotionItem : cartPromotionItemList) {
            // 这里通过DAO层方法锁定库存,使用数据库行锁,确保原子性
            int count = portalOrderDao.lockStockBySkuId(cartPromotionItem.getProductSkuId(),cartPromotionItem.getQuantity());
            if(count==0){
                Asserts.fail("库存不足,无法下单");
            }
        }
    }
    
  3. 在PortalOrderDao中,lockStockBySkuId的SQL实现(mall-portal/src/main/resources/com/macro/mall/portal/dao/PortalOrderDao.xml):

    <update id="lockStockBySkuId">
        UPDATE pms_sku_stock
        SET lock_stock = lock_stock + #{quantity}
        WHERE
            id = #{productSkuId}
            AND lock_stock + #{quantity} &lt;= stock
    </update>
    

    这个SQL语句在更新时会检查锁定后的库存是否小于等于实际库存,如果条件不满足,更新操作不会执行,返回影响行数为0,从而抛出异常。

  4. 支付成功后,扣减真实库存(OmsPortalOrderServiceImpl.paySuccess方法):

    // 恢复所有下单商品的锁定库存,扣减真实库存
    OmsOrderDetail orderDetail = portalOrderDao.getDetail(orderId);
    int totalCount = 0;
    for (OmsOrderItem orderItem : orderDetail.getOrderItemList()) {
        int count = portalOrderDao.reduceSkuStock(orderItem.getProductSkuId(),orderItem.getProductQuantity());
        if(count==0){
            Asserts.fail("库存不足,无法扣减!");
        }
        totalCount+=count;
    }
    
  5. reduceSkuStock的SQL实现(同样在PortalOrderDao.xml中):

    <update id="reduceSkuStock">
        UPDATE pms_sku_stock
        SET lock_stock = lock_stock - #{quantity},
            stock = stock - #{quantity}
        WHERE
            id = #{productSkuId}
            AND stock - #{quantity} &gt;= 0
            AND lock_stock - #{quantity} &gt;= 0
    </update>
    

    这里扣减真实库存,同时减少锁定库存,并且检查库存和锁定库存都不为负。

  6. 如果订单取消,则释放锁定库存(OmsPortalOrderServiceImpl.cancelOrder方法):

    // 解除订单商品库存锁定
    if (!CollectionUtils.isEmpty(orderItemList)) {
        for (OmsOrderItem orderItem : orderItemList) {
            int count = portalOrderDao.releaseStockBySkuId(orderItem.getProductSkuId(),orderItem.getProductQuantity());
            if(count==0){
                Asserts.fail("库存不足,无法释放!");
            }
        }
    }
    
  7. releaseStockBySkuId的SQL实现:

    <update id="releaseStockBySkuId">
        UPDATE pms_sku_stock
        SET lock_stock = lock_stock - #{quantity}
        WHERE
            id = #{productSkuId}
            AND lock_stock - #{quantity} &gt;= 0
    </update>
    

综上所述,防止超卖的措施包括:

  • 下单时先锁定库存(更新锁定库存字段),利用数据库的行锁和条件更新确保原子性。
  • 支付成功后扣减真实库存,同时减少锁定库存。
  • 如果订单取消,则释放锁定库存。

这样,通过锁定库存的方式,保证了在并发情况下,库存不会超卖。因为每个下单请求都会尝试锁定库存,如果锁定失败(库存不足)则下单失败。支付成功后才会真正扣减库存。

注意:这里锁定库存和扣减库存都是在数据库层面通过更新语句的条件判断来保证的,避免了在应用层计算可能出现的并发问题。

库存防超卖机制总结

1. 双重库存检查机制

// 下单前检查真实库存
private boolean hasStock(List<CartPromotionItem> cartPromotionItemList) {
    for (CartPromotionItem cartPromotionItem : cartPromotionItemList) {
        if (cartPromotionItem.getRealStock() == null 
            || cartPromotionItem.getRealStock() <= 0 
            || cartPromotionItem.getRealStock() < cartPromotionItem.getQuantity()) {
            return false;
        }
    }
    return true;
}

2. 库存锁定机制

// 下单时锁定库存
private void lockStock(List<CartPromotionItem> cartPromotionItemList) {
    for (CartPromotionItem cartPromotionItem : cartPromotionItemList) {
        int count = portalOrderDao.lockStockBySkuId(
            cartPromotionItem.getProductSkuId(), 
            cartPromotionItem.getQuantity()
        );
        if(count == 0){
            Asserts.fail("库存不足,无法下单");
        }
    }
}

3. 数据库层面防超卖

<!-- 锁定库存SQL,确保原子性操作 -->
<update id="lockStockBySkuId">
    UPDATE pms_sku_stock 
    SET lock_stock = lock_stock + #{quantity}
    WHERE id = #{productSkuId}
    AND lock_stock + #{quantity} &lt;= stock
</update>

<!-- 扣减库存SQL,多重条件检查 -->
<update id="reduceSkuStock">
    UPDATE pms_sku_stock
    SET lock_stock = lock_stock - #{quantity},
        stock = stock - #{quantity}
    WHERE id = #{productSkuId}
    AND stock - #{quantity} &gt;= 0
    AND lock_stock - #{quantity} &gt;= 0
</update>

4. 库存状态管理

系统维护两种库存状态:

  • stock: 实际库存数量
  • lock_stock: 锁定库存数量(已下单未支付)

5. 事务性保证

支付成功后才会真正扣减库存:

// 支付成功后扣减真实库存
public Integer paySuccess(Long orderId, Integer payType) {
    // 只修改未付款状态的订单
    int updateCount = orderMapper.updateByExampleSelective(order, orderExample);
    if(updateCount == 0){
        Asserts.fail("订单不存在或订单状态不是未支付!");
    }
    
    // 扣减真实库存
    for (OmsOrderItem orderItem : orderDetail.getOrderItemList()) {
        int count = portalOrderDao.reduceSkuStock(
            orderItem.getProductSkuId(),
            orderItem.getProductQuantity()
        );
        if(count == 0){
            Asserts.fail("库存不足,无法扣减!");
        }
    }
    return totalCount;
}

6. 超时订单库存释放

// 取消超时订单,释放锁定库存
public Integer cancelTimeOutOrder() {
    for (OmsOrderDetail timeOutOrder : timeOutOrders) {
        // 解除订单商品库存锁定
        portalOrderDao.releaseSkuStockLock(timeOutOrder.getOrderItemList());
    }
    return timeOutOrders.size();
}

防超卖流程总结

  1. 下单前检查:验证真实库存是否足够
  2. 下单时锁定:原子性锁定库存,防止并发超卖
  3. 支付时扣减:支付成功后才扣减真实库存
  4. 超时释放:未支付订单超时后自动释放锁定库存
  5. 数据库约束:通过SQL条件确保库存不会为负

这种机制通过"预占库存→支付确认→实际扣减"的三阶段流程,有效防止了库存超卖问题。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐