一、前言
在不少商城,零售项目中,库存模块是比不可少的,如何保证商品不被卖超,数据正确性。
那我们在淘宝提交一笔订单,后台商品库存究竟发生了哪些变化呢?
今天举一个简单的例子说一下库存相关的一些东西,不正之处也请多多指教。
二、库表设计
商品的具体属性就不过多说,这里只针对商品库存表,大家也只可以关注商品的实物库存字段stock和锁定库存字段low_stock
三、流程图
当我们提交一笔订单,包含商品A一共5件,还没有付款之前,后端会将该笔订单商品的数量锁定,即low_stock增加5。
付款成功之后,一般的处理就是将stock和low_stock同时扣减5.
如果通过各种延迟任务系统自动取消订单或者用户手动取消订单,这时候只需要扣减锁定库存即可,即low_stock减5。
四、代码实现
由上续分析,库存相关方面的核心代码主要是涉及到 增加锁定库存(提交订单),释放锁定库存(取消订单),库存扣减(订单交易成功)
/**
* 锁定库存
*
* @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测试一下。
- 先测试两个用户操作不同的商品
可以看到用户A在请求251和252商品,并处于休眠状态下,用户B请求不同的商品249和250,成功获取到锁,并且进到业务代码里面。
- 再来测试用户A和用户B的订单包含同一种商品 用户A请求商品不变依旧是251和252,用户B请求251和250。
可以看到用户B这里获取锁失败,提示稍后重试。
七、最后
释放锁定库存(取消订单),库存扣减(订单交易成功)加锁的实现方案一样,这里可以采用切面编程去优化代码,因为这里的获取锁,释放锁的代码一样,重复编写看起来十分臃肿。