电商项目中的库存扣减实现方案加代码实现

175 阅读4分钟

一、前言

在不少商城,零售项目中,库存模块是比不可少的,如何保证商品不被卖超,数据正确性。

那我们在淘宝提交一笔订单,后台商品库存究竟发生了哪些变化呢?

今天举一个简单的例子说一下库存相关的一些东西,不正之处也请多多指教。

二、库表设计

商品的具体属性就不过多说,这里只针对商品库存表,大家也只可以关注商品的实物库存字段stock和锁定库存字段low_stock

image.png

三、流程图

当我们提交一笔订单,包含商品A一共5件,还没有付款之前,后端会将该笔订单商品的数量锁定,即low_stock增加5。

付款成功之后,一般的处理就是将stock和low_stock同时扣减5.

如果通过各种延迟任务系统自动取消订单或者用户手动取消订单,这时候只需要扣减锁定库存即可,即low_stock减5。

image.png

四、代码实现

由上续分析,库存相关方面的核心代码主要是涉及到 增加锁定库存(提交订单),释放锁定库存(取消订单),库存扣减(订单交易成功)

    /**
     * 锁定库存
     *
     * @param skuStockQoList  这笔订单包含的商品列表
     * @throws CommonException
     */
private void lockSkuStockHandler(List<SkuStockQo> skuStockQoList) throws CommonException {
    
    // 去数据库查询当前的实物库存和锁定库存
    List<SkuStock> stockList = list(new LambdaQueryWrapper<SkuStock>().in(SkuStock::getId,
                                                                          skuStockQoList.stream().map(SkuStockQo::getProductSkuId).collect(Collectors.toList())));
    if (CollectionUtils.isEmpty(stockList)) {
        return;
    }
    
    // 转成map
    Map<Long, SkuStock> stockMap = stockList.stream().collect(Collectors.toMap(SkuStock::getId, Function.identity()));
    if (MapUtils.isEmpty(stockMap)) {
        return;
    }
​
    List<SkuStock> udpateList = Lists.newArrayList();
    for (SkuStockQo item : skuStockQoList) {
        SkuStock skuStock = stockMap.get(item.getProductSkuId());
        if (Objects.isNull(skuStock)) {
            continue;
        }
        // 如果当前数据库里面的锁定库存+该笔订单的锁定数量 > 实物库存  即库存不足
        if ((skuStock.getLockStock().longValue() + item.getLockQuantity().longValue()) > skuStock.getStock()) {
            throw new CommonException("商品" + skuStock.getSkuCode() + "库存不足");
        }
        
        // 通过校验,正常增加锁定库存
        udpateList.add(new SkuStock().setId(skuStock.getId()).setLockStock(skuStock.getLockStock().longValue() + item.getLockQuantity().longValue()));
    }
​
    if (CollectionUtils.isEmpty(udpateList)) {  
            return;  
    }
    // 更新数据库
    updateBatchById(udpateList);
​
}
​
​
    /**
     * 解锁库存
     *
     * @param skuStockQoList
     * @throws CommonException
     */
private void unLockSkuStockHandler(List<SkuStockQo> skuStockQoList) throws CommonException {
    
        // 去数据库查询当前的实物库存和锁定库存
        List<SkuStock> stockList = list(new LambdaQueryWrapper<SkuStock>().in(SkuStock::getId,
                skuStockQoList.stream().map(SkuStockQo::getProductSkuId).collect(Collectors.toList())));
        if (CollectionUtils.isEmpty(stockList)) {
            return;
        }
    
        // 转成map
        Map<Long, SkuStock> stockMap = stockList.stream().collect(Collectors.toMap(SkuStock::getId, Function.identity()));
        if (MapUtils.isEmpty(stockMap)) {
            return;
        }
        List<SkuStock> udpateList = Lists.newArrayList();
        for (SkuStockQo item : skuStockQoList) {
            SkuStock skuStock = stockMap.get(item.getProductSkuId());
            if (Objects.isNull(skuStock)) {
                continue;
            }
            
            //
            if (skuStock.getLockStock().longValue() < item.getUnlockQuantity().longValue()) {
                throw new CommonException("商品" + skuStock.getSkuCode() + "解锁数量异常");
            }
            
            // 通过校验,正常扣减锁定库存
            udpateList.add(new SkuStock().setId(skuStock.getId()).setLockStock(skuStock.getLockStock().longValue() - item.getUnlockQuantity().longValue()));
        }
        
        if (CollectionUtils.isEmpty(udpateList)) {  
            return;  
        }
        // 更新数据库
        updateBatchById(udpateList);
}
​
​
    /**
     * 扣减库存
     *
     * @param skuStockQoList
     * @throws CommonException
     */
private void reduceSkuStockHandler(List<SkuStockQo> skuStockQoList) {
    
        // 去数据库查询当前的实物库存和锁定库存
        List<SkuStock> stockList = list(new LambdaQueryWrapper<SkuStock>().in(SkuStock::getId,
                skuStockQoList.stream().map(SkuStockQo::getProductSkuId).collect(Collectors.toList())));
        if (CollectionUtils.isEmpty(stockList)) {
            return;
        }
        
        // 转成map
        Map<Long, SkuStock> stockMap = stockList.stream().collect(Collectors.toMap(SkuStock::getId, Function.identity()));
        if (MapUtils.isEmpty(stockMap)) {
            return;
        }
        List<SkuStock> udpateList = Lists.newArrayList();
        for (SkuStockQo item : skuStockQoList) {
            SkuStock skuStock = stockMap.get(item.getProductSkuId());
            if (Objects.isNull(skuStock)) {
                continue;
            }
            // 扣减实物库存和锁定库存
            udpateList.add(new SkuStock().setId(skuStock.getId())
                    .setLockStock(skuStock.getLockStock().longValue() - item.getLockQuantity().longValue())
                    .setStock(skuStock.getStock().longValue() - item.getLockQuantity().longValue()));
        }
    
        if (CollectionUtils.isEmpty(udpateList)) {  
            return;  
        }
        // 更新数据库
        updateBatchById(udpateList);
}

其中SkuStockQo结构

@Data
@Accessors(chain = true)
public class SkuStockQo {
​
    private Long productSkuId;  // 该商品skuId
​
    private Long lockQuantity;  // 商品购买数量
​
    private Long unlockQuantity; // 商品购买数量
}

五、问题

我们知道,上续代码实现业务逻辑应该没有什么问题,但是在如果高用户的情况下,因为业务逻辑操作不是原子性且没有加任何锁,所以很容易出现商品卖超的情况,传统的java锁lock和synchronized在单机情况下都是可以处理的,但是目前基本上都是集群部署来面对高用户群体。所以就引出了分布式锁,分布式锁的实现方案很多具体可以参考juejin.cn/post/684490…,这里我们使用的是RedissonClient。

以增加锁定库存为例,代码实现

private final String PRE_SKU_STOCK = "pre_sku_stock";
​
@Resource
private RedissonClient redissonClient;
​
public void lockSkuStock(List<SkuStockQo> skuStockQoList) throws CommonException {
        if (CollectionUtils.isEmpty(skuStockQoList)) {
            return;
        }
    
        // 这里用的是组合锁,因为要保证这笔订单中每一个商品在这一刻只有一共用户在操作
        RLock multiLock = redissonClient.getMultiLock(skuStockQoList.stream().map(
                item -> redissonClient.getLock(PRE_SKU_STOCK + item.getProductSkuId())
        ).toArray(RLock[]::new));
​
​
        try {
            // 非阻塞
            if (multiLock.tryLock(3, TimeUnit.SECONDS)) {
​
                // 获取组合锁成功
                try {
                    lockSkuStockHandler(skuStockQoList);
                } finally {
                    // 解锁
                    multiLock.unlock();
                }
​
            } else {
                // 获取组合锁失败
                throw new CommonException("请稍后重试");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
}

六、测试

我们起两个服务8082和8081,并且在8082的业务逻辑那里休眠20s,模拟多用户请求操作同一个商品库存。 这里我们用postman测试一下。

  • 先测试两个用户操作不同的商品

image.png

image.png 可以看到用户A在请求251和252商品,并处于休眠状态下,用户B请求不同的商品249和250,成功获取到锁,并且进到业务代码里面。

  • 再来测试用户A和用户B的订单包含同一种商品 用户A请求商品不变依旧是251和252,用户B请求251和250。

image.png

image.png

可以看到用户B这里获取锁失败,提示稍后重试。

七、最后

释放锁定库存(取消订单),库存扣减(订单交易成功)加锁的实现方案一样,这里可以采用切面编程去优化代码,因为这里的获取锁,释放锁的代码一样,重复编写看起来十分臃肿。