场景描述
无论是日常工作中,还是面试问题中,并发扣库存都是一个很常见的场景,正好业务里有这样的场景,可以对这类问题做一下总结。
举例说明两个比较典型的扣库存场景:
- 产品1:线上招募人员的产品,招募是有人数限制的,每招募成功1人,扣减库存1,直到库存为0,自动停止招募。
- 产品2:用户秒杀产品,用户在同一个时间点,同时抢一件有库存的商品。
一般来说,一次扣库存,可以分解为以下3个动作:
- 查询最新库存(query)
- 检查库存是否足够(内存计算)
… - 更新库存(update)
由扣库存产生的问题:
- 单用户重复提交未做幂等
用户同时提交了2次扣库存操作,因未做幂等,导致一次购买扣减2次库存。 - 多用户并发场景下库存超卖
2个用户抢1个库存,查询到的库存都是1,检查库存足够,然后都去扣减库存,库存变成-1,发生了超卖。
对于问题1:重复提交的幂等性问题,该问题比较容易解决:
1.前端做防重机制防止用户二次提交请求。
2.后端做幂等处理,用户请求带业务token,进行校验,重复token直接返回。
这里主要讨论超卖的问题,我们从各个方面一一探讨解决方案。
假设最原始的,不采用任何解决方案的代码如下:
@Transactional(propagation=Propagation.REQUIRED)
@Override
public void decreaseItemSpecStock(String specId,Integer buysCounts){
//1.查询库存
int stock=10;//假设查询数据库后,其值为10.
//2.判断库存,是否能够扣除
if(stock<buysCounts){
//提示用户库存不够
}
//3.扣库存
...
}
扣库存问题不推荐的方案:
-
使用
synchronized
关键字
虽然synchronized
关键字在单体架构上能够很好的工作,但是如果使用集群架构,就不能产生想要的效果了。
其次,方法上加锁对性能影响较大,导致单体架构性能低下。 -
锁数据库或者数据库表(添加全局锁或者行锁)
这种方法也不推荐,首先性能会降低,其次如果出现事故不好处理。 -
悲观锁,将扣库存操作(step1->3)变成只能串行,缺点是同一时间只能有一个用户来操作库存,导致并发量不高(无论是通过synchronized关键字、数据库锁比如select for update、分布式锁等各种方式加锁,本质都是一样的)
扣库存问题解决方案推荐:
- 直接扣库存,不预检查库存
update stock_table set stock = stock - 1 where stock-1 >= 0 and id = xxx
,缺点是不通用,比如业务上要求库存除了有reduce还有add操作。
大部分简单业务场景下,方法1完全够用了,甚至一些对并发并不是特别高、业务容忍少量超卖场景下,直接扣库存,无需检查库存是否stock-1 >= 0。
我们这里代码使用了@Transactional
,所以如果数据库执行失败,可以进行回滚。
@Transactional(propagation=Propagation.REQUIRED)
@Override
public void decreaseItemSpecStock(String specId,Integer buysCounts){
//直接扣库存
int result=itemsMapperCustom.decreaseStock(specId,busCounts);
if(result!=1){
throw new RuntimeException("订单创建失败,库存不足");
}
}
-
乐观锁CAS方案
相比较直接扣库存,库存增加版本字段version
,在更新库存时比较版本号例如update stock_table set version = old_version + 1,stock = stock - 1 where version = query_version and id = xxx
,只有版本号没有变化,才能更新库存成功,如果版本号发生变化,则更新库存失败并进行重试。 -
分布式锁:zookeeper redis
库存放到redis等缓存中,在redis中进行库存的查询、扣减,利用内存数据库的特性提高读QPS。对DB进行水平扩展(分库分表方案)来提升读写QPS等等。
//todo 其细节以后再添加
,这里简单看下代码就能理解:
@Transactional(propagation=Propagation.REQUIRED)
@Override
public void decreaseItemSpecStock(String specId,Integer buysCounts){
lockUtil().getLock(); //--分布式加锁
//1.查询库存
int stock=10;//假设查询数据库后,其值为10.
//2.判断库存,是否能够扣除
if(stock<buysCounts){
//提示用户库存不够
}
//3.扣库存
...
lockUtil().unLock();//--分布式解锁
}
如何验证
最简单粗暴的方法就是构造大流量压测:
1.第一个幂等问题,对单用户请求大流量压测,基本都能发现问题。
2.第二个多用户并发问题,多个用户的请求大流量压测,也能发现问题。
扩展下:
1个商品有多个库存,怎么处理?