【面试题】商品超买超卖问题分析及实战

项目场景:

原文链接

商品超买超卖是高并发下非常典型的问题,也是面试中秒杀场景常常会问到的问题。
常见的问题有:
1、怎么设计一个秒杀系统?
2、商品超买、超卖问题产生的原因?
3、怎么防止商品出现超买|超卖问题?
4、乐观锁和悲观锁的适用场景是什么?
5、提高事务的隔离级别能解决超买|超卖问题吗?

今天和大家一起探究下商品超买、超卖的原因及其解决方案。


原因分析:

商品下单扣减库存的流程如下:
1、根据商品ID查询商品库存信息
2、判断商品库存是否大于购买数量
3、库存充足则进行下单减库存操作

模拟代码如下:

@Transactional(rollbackFor = Exception.class)
public void secKill(Integer goodsId, Integer num) throws InterruptedException {
   
        //1、查询商品库存
        GoodsStock  goodsStock  = goodsStockMapper.getStock(goodsId);
        //2、判断库存是否充足
        if(goodsStock.getNum() >= num){
   
            //3、使用减法计算出剩余库存
            int stockNum = goodsStock.getNum() - num;
            goodsStock.setNum(stockNum);
            //4、更新商品剩余库存的值
            int result = goodsStockMapper.updateByPrimaryKeySelective(goodsStock);
            if(result<0){
   
                 log.error("库存不足");
                 throw new RuntimeException("秒杀失败");
            }else{
   
                log.info("秒杀成功");
            }
        }else{
   
            log.error("库存不足");
            throw new RuntimeException("秒杀失败");
        }
    }

采用jMeter压测发现,这样的代码不但会出现超买超卖的问题,还会导致商品的剩余库存出现覆盖更新的情况。

流程分析:
1、在高并发的情况下,会有很多请求同时查询到商品的库存信息,进入到步骤1。
2、并通过了库存是否充足的判断,计算出剩余库存。
3、通过updateByPrimaryKeySelective方法直接将计算出的剩余库存的值写入到数据库。
假设A商品当前剩余库存是10,有10个线程同时进入到步骤1下单购买10个A商品,刚好都通过了步骤2的库存是否充足的判定,经过步骤3计算出剩余库存为0,然后执行更新操作将剩余库存的值写入到数据库。
最后我们发现,明明A商品只有10件,但是我们确卖出了100件。这就是商品的超买、超卖问题。

下面是模拟2个并发事务购买10个A商品的请求过程:
A商品库存只有10个,最后却卖出了20件。
在这里插入图片描述


原因说明:

1、添加事务控制并不能保证减库存方法secKill()执行的原子性,代码仍然会并发执行。
2、在并发场景下,先查询库存,再用java代码判定库存是否充足是不正确。
3、在并发场景下,如果先通过java代码计算出剩余库存,再把剩余库存的值更新到数据库中会导致出现覆盖更新的情况。


解决方案:

1、最简单暴力的办法,既然减库存的方法secKill()不支持并发,那么可以将整个方法块做异步处理。如果不考虑分布式,可以直接用synchronized关键字,如果需要支持分布式,可以采用redis加分布式锁。

2、采用悲观锁,给数据库记录加锁。

3、采用乐观锁,更新记录时通过比对版本号判定是否执行更新库存。

4、直接在sql中执行减法来更新库存,并在where调价中判定库存是否充足

update table set stock = stock - 10  where goods_id=1 and stock - 10 >=0 ;


代码实战:

1、商品库存表

CREATE TABLE `goods_stock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `goods_id` varchar(255) DEFAULT NULL COMMENT '商品id',
  `num` int(11) DEFAULT NULL COMMENT '库存数量',
  `version` int(11) unsigned DEFAULT NULL COMMENT '版本号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ui_goods_id` (`goods_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

INSERT INTO `seckill`.`goods_stock`(`id`, `goods_id`, `num`, `version`) VALUES (1, '1', 10000, NULL);

2、控制层代码 SecKillController

@RestController
@Slf4j
public class SecKillController {
   

    @Autowired
    private SecKillService secKillService;

    @GetMapping(value = "/secKill/{goodsId}/{num}")
    public void secKill(@PathVariable Integer goodsId, @PathVariable Integer num) throws InterruptedException{
   
        secKillService.secKil
  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值