电商场景:并发扣库存,怎么保证不超卖又不影响并发性能

数据库技术
406
0
0
2022-04-12

任何电商平台的一个主业务场景就是:

  1. 加入购物车;
  2. 去结算,填写/选择收货地址;
  3. 检查/扣减库存,生成订单并付款;

其中第三步,检查/扣减库存,常规代码实现如下:

判断剩余库存量,如果库存足够,则做扣减操作;

select stock from goods_stock where sku = 'a1'
if(stock - buy_num) >= 0){ //如果库存大于等于购买数量
 stock = stock - buy_num;
 update goods_stock set stock = stock where sku = 'a1'
}

一般来讲,一张订单不会只买一种商品,如果有多个,则需要重复上述

这种方式最大的问题就是不支持并发,哪怕并发数是2个,都有可能出现库存扣减错误;

因为获取剩余库存与实际update 有个时间差,假设此时有两个下单请求,都购买a1,此时剩余库存为1,这两个下单请求同时 select 请求到了 剩余的1个库存,都能顺利的执行后续的操作,最终会卖出2个,造成超卖;

解决这个问题,最简单的方式就是加入事务,但是,事务会造成锁表、锁行,效率低下;

一种解决方式:在update的时候,加入一个条件:

update goods_stock set stock = stock where sku = 'a1' and stock = old_stock

更直接的一种方式如下,不用先查询剩余库存,直接update

update goods_stock set stock = stock - buy_num where sku = 'a1' and (stock-buy_num) >= 0 ;

因为update 会把并发串行执行,高并发下,库存不足时,会执行失败;

现在设想这么一个场景,1万用户,抢购N种商品,每种商品库存100件。每个用户可能都会同时抢购n种商品;

按实际经验估算,3万用户以下,利用mysql的update机制,足够应付,此处不考虑更大并发的情况,更大并发场景需要引入redis等解决方案了,后面专门发文阐述;

考虑事务效率不佳,此处我们抛弃掉事务,引入队列来实现库存扣减失败的回滚;

boolean isSuccess = true;
for (sku in goods){
 if(update goods_stock set stock = stock - sku[num] where sku = sku[sn] and (stock-sku[num]) >= 0 ){
 sku_success[] = {sku[sn],sku[num]}
 }else{
 //扣减失败
 push_queue(sku_success[]) //队列存入需要回滚的商品编码及对应数量,异步处理;
 isSuccess = false;
 }
}
if (isSuccess){
 //生成订单
 createOrder(); 
}else{
 //抛出错误提示:某商品库存不足,无法生成订单;
}

说明:文中 用户数 != 并发数,一般抢购用户多集中于第一分钟,在高并发下,一般认为没有出现响应超过500ms的接口,说明系统工作正常,用户体验合格,用户基本在30秒内能够完成浏览商品,加入购物车,下单的操作。

以上场景在不引入redis的情况下,仅利用mysql的锁机制,承受3万用户一分钟的高访问,产生6万-10万单是可行的。

如果并发场景要求更高,能采用的方式 可以是 mysql 多主多从,引入redis/zk 等。