任何电商平台的一个主业务场景就是:
- 加入购物车;
- 去结算,填写/选择收货地址;
- 检查/扣减库存,生成订单并付款;
其中第三步,检查/扣减库存,常规代码实现如下:
判断剩余库存量,如果库存足够,则做扣减操作;
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 等。