引言
在电商业务中,库存超卖问题就如同一颗定时炸弹,随时可能在高并发的环境下引爆。对于后端工程师来说,就需要为这颗炸弹加上防止爆炸的枷锁,从而避免因为超卖导致的资损问题。本系列文章就将从这个场景入手,一步步地为各位读者引入分布式锁的各种实现,从而让大家可以掌握分布式锁在常见场景的使用。
需求背景
背景非常简单,就是在电商项目中,用户购买商品和数量后后,系统会对商品的库存进行相应数量的扣减。因此,我们模拟这个场景就需要商品表和库存表两张表,但业务并不是这里的重点,需要简化一下,一张简单的商品库存表足以,如下:
sql
代码解读
复制代码
CREATE TABLE `tb_goods_stock` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `goods_id` bigint(20) NOT NULL COMMENT '商品id', `stock` int NOT NULL COMMENT '库存数', PRIMARY KEY (`id`) ) COMMENT = '商品库存表';
接着,我们创建一个SpringBoot
的项目,在接口中实现简单的扣减库存的逻辑,示例如下:
java
代码解读
复制代码
public String deductStock(Long goodsId,Integer count){ //1.查询商品库存的库存数量 Integer stock = stockDao.selectStockByGoodsId(goodsId); //2.判断商品的库存数量是否足够 if (stock < count) return "库存不足"; //3.如果足够,扣减库存数量 stockDao.updateStockByGoodsId(goodsId,stock-count); //4.返回扣减成功 return "库存扣减成功!"; }
创建成功后,先往数据库里插入一条商品id为1、库存为1的数据,便于我们测试接口的逻辑。分别执行两次调用,分别得到库存不足和库存扣减成功的提示,验证逻辑没有问题,如下:
发现问题
上面的例子如果是通过单词访问,那么它的执行结果也是符合我们预期的。但在高并发场景下,多个线程同时访问同一个数据就可能出现超卖问题。因此,我们用JMeter
来模拟大量并发数据来进行线上抢购场景复现,如下:
添加一个线程组,设定50个线程和100次循环次数,如下:
这时再将数据库里的商品id为1的数据的库存修改为5000
,如下:
接着执行HTTP请求,如下:
通过聚合报告可以看出5000次请求都执行成功,这个时候按照正常逻辑,库存应该扣完了,回到数据库查询,如下:
通过查询发现还有4000多个库存,带换到线上场景,这个时候后续还有用户继续请求购买,最终实际卖出的肯定会远远超过库存,这就是经典的超卖问题。
JVM锁初显神通
并发问题去找锁
这个几乎是大家的共识,那么这里的超卖问题也不例外。因此,最直接的办法就是直接在涉及扣减库存的逻辑或操作上进行加锁
处理。首先,最先想到的就是JVM锁,只需要一个synchronized
关键字就可以实现,代码修改如下:
java
代码解读
复制代码
public synchronized String deductStock(Long goodsId,Integer count){ //1.查询商品库存的库存数量 Integer stock = stockDao.selectStockByGoodsId(goodsId); //2.判断商品的库存数量是否足够 if (stock < count) return "库存不足"; //3.如果足够,扣减库存数量 stockDao.updateStockByGoodsId(goodsId,stock-count); //4.返回扣减成功 return "库存扣减成功!"; }
我们这时候去把数据库的库存还原下,然后重新用JMeter
进行请求(Ps:原参数不变),执行后我们先看数据库结果,如下:
可以看到这次的库存就被扣减完了,但我们查看聚合报告会发现对比前面的请求,有一项指标下降了很多-吞吐量,从三千多到现在的一千多,所以加锁肯定对性能是会