先看问题
首先上一串代码
public String buy(Long goodsId, Integer goodsNum) {
//查询商品库存
Goods goods = goodsMapper.selectById(goodsId);
//如果当前库存为0,提示商品已经卖光了
if (goods.getGoodsInventory() <= 0) {
return "商品已经卖光了!";
}
//如果当前购买数量大于库存,提示库存不足
if (goodsNum > goods.getGoodsInventory()) {
return "库存不足!";
}
//更新库存
goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
goodsMapper.updateById(goods);
return "购买成功!";
}
我们看一下这串代码,逻辑用流程图表示如下:
从图上看,逻辑还是很清晰明了的,而且单测的话,也测试不出来什么bug。但是在秒杀场景下,问题可就大发了,100件商品可能卖出1000单,出现严重资损,这下就真的需要杀个程序员祭天了。
问题分析
正常情况下,如果请求是一个一个接着来的话,这串代码也不会有问题,如下图:
不同的时刻不同的请求,每次拿到的商品库存都是更新过之后的,逻辑是ok的。
那为啥会出现超卖问题呢? 首先我们给这串代码增加一个场景:商品秒杀(非秒杀场景难以复现超卖问题)。 秒杀场景的特点如下:
- 高并发处理:秒杀场景下,可能会有大量的购物者同时涌入系统,因此需要具备高并发处理能力,保证系统能够承受高并发访问,并提供快速的响应。
- 快速响应:秒杀场景下,由于时间限制和竞争激烈,需要系统能够快速响应购物者的请求,否则可能会导致购买失败,影响购物者的购物体验。
- 分布式系统: 秒杀场景下,单台服务器扛不住请求高峰,分布式系统可以提高系统的容错能力和抗压能力,非常适合秒杀场景。
在这种场景下,请求不可能是一个接一个这种,而是成千上万个请求同时打过来,那么就会出现多个请求在同一时刻查询库存,如下图:
如果在同一时刻查询商品库存表,那么得到的商品库存也肯定是相同的,判断的逻辑也是相同的。
举个例子,现在商品的库存是10件,请求1买6件,请求2买5件,由于两次请求查询到的库存都是10,肯定是可以卖的。 但是真实情况是5+6=11>10,明显有问题!这两笔请求必然有一笔失败才是