java_高并发之解决超卖的4种方案

【每天学一点,总有一天熬成大师】

解决超卖的4种方案

本文内容

  • 了解超卖本质解决方案,也是并发修改数据的本质解决方案
  • 掌握4种方案解决超卖问题(原理、源码、测试用例)
  • 每个方案都有测试用例,会模拟并发秒杀,带大家看效果
  • 学完后,完全可以解决工作中所有并发修改数据出错的问题,太硬核了吧。。。

避免超卖,避免并发修改数据出错,有银弹吗?

本质上是需要加锁,不管是什么锁,只要让减库存的操作排队,便可解决超卖问题,核心点就是:加锁排队

同理:解决并发修改数据出错问题,最终也是靠锁解决,比如乐观锁、悲观锁,本质上都是要靠锁,让并发问题排队执行,只是这个锁的范围大小的问题。

言归正传,下面咱们上方案、原理、源码、测试用例,一个都不能少。

商品表

-- 商品表
create table if not exists t_goods
(
    goods_id   varchar(32) primary key comment '商品id',
    goods_name varchar(256) not null comment '商品名称',
    num        int          not null comment '库存',
    version    bigint default 0 comment '系统版本号'
) comment = '商品表';

解决超卖方案1:通过update中携带条件判断解决超卖问题

原理

通过下面sql的执行结果,便可确保超卖问题,重点在于需要在update的where条件中加上库存扣减后不能为0,sql会返回影响行数,如果影响行数为0,表示库存不满足要求,扣减失败了,否则,扣减库存成功。

String goodsId = "商品id";
int num = "本次需要扣减的库存量";

// count表示影响行数
int count = (update t_goods set num = num - #{num} where goods_id = #{goodsId} and num - #{num} >= 0);

// count = 1,表示扣减成功,否则扣减失败
if(count==1){
	//扣减库存成功
}else{
	//扣减库存失败
}

源码

com.service.GoodsServiceImpl#placeOrder1

运行看结果

===========================解决超卖,方案1 开始执行=======================================
模拟 100 人进行抢购
抢购结束啦............

抢购前,商品库存:10
抢购后,商品库存:0
下单成功人数:10
下单失败人数:90
===========================解决超卖,方案1 执行结束=======================================

解决超卖方案2:使用乐观锁版本号解决这个问题

原理

需要在库存表加一个version字段,这个version每次更新的时候需要+1,单调递增的。

业务逻辑如下

String goodsId = "商品id";
int num = "本次需要扣减的库存量";
GoodsPo goods = (select * from t_goods where goods_id = #{goodsId});

// 期望数据库中该数据的version值
int expectVersion = goods.getVerion();

//乐观锁更新数据,where条件中必须带 version = #{expectVersion}
int count = update t_goods set num = num - ${num}, version = version + 1 where goods_id = #{goodsId} and version = #{expectVersion}

// count = 1,表示扣减成功,否则扣减失败
if(count==1){
	//扣减库存成功
}else{
	//扣减库存失败
}

源码

com.service.GoodsServiceImpl#placeOrder2

运行结果

===========================解决超卖,方案2 开始执行=======================================
模拟 100 人进行抢购
抢购结束啦............

抢购前,商品库存:10
抢购后,商品库存:0
下单成功人数:10
下单失败人数:90
===========================解决超卖,方案2 执行结束=======================================

解决超卖方案3:对比数据修改前后是否和期望的一致

原理

String goodsId = "商品id";
int num = "本次需要扣减的库存量";

//扣减库存前,查出商品库存数量,丢到变量 beforeGoodsNum 中
GoodsPo beforeGoods = (select * from t_goods where goods_id = #{goodsId});
int beforeGoodsNum = beforeGoods.num;

// 执行扣减库存操作,条件中就只有goodsId,说明这个可能将库存扣成负数,出现超卖,继续向下看,后面的步骤将解决超卖
update t_goods set num = num - ${购买的商品数量} where goods_id = #{goodsId}

//扣减库存后,查出商品库存数量,丢到变量 afterGoodsNum 中
GoodsPo afterGoods = (select * from t_goods where goods_id = #{goodsId});
int afterGoodsNum = afterGoods.num;

// 如下判断,库存扣减前后和期望的结果是不是一致的,扣减前的数据 - 本次需要扣减的库存量 == 扣减后的数量,如果是,说明没有超卖
if(beforeGoodsNum - num == afterGoodsNum){
	//扣减库存成功
}else{
	//扣减库存失败
}

这种方案虽然看起来很奇怪,但是有些业务场景中,可以解决一些问题,比如批量去修改数据,想判断批量的过程中,数据是否被修改过,可以通过这种方式判断。

源码

com.service.GoodsServiceImpl#placeOrder3

运行结果

===========================解决超卖,方案3 开始执行=======================================
模拟 100 人进行抢购
抢购结束啦............

抢购前,商品库存:10
抢购后,商品库存:0
下单成功人数:10
下单失败人数:90
===========================解决超卖,方案3 执行结束=======================================

解决超卖方案4:通过辅助类解决超卖问题

原理

需要添加一张辅助表(t_concurrency_safe),如下,这张表需要有版本号字段,通过这张表的乐观锁,将需要保护的业务方法包起来,解决超卖问题。

create table if not exists t_concurrency_safe
(
    id       varchar(32) primary key comment 'id',
    safe_key varchar(256) not null comment '需要保护的数据的唯一的key',
    version  bigint default 0 comment '系统版本号,默认为0,每次更新+1',
    UNIQUE KEY `uq_safe_key` (`safe_key`)
) comment = '并发安全辅助表';

逻辑如下

String goodsId = "商品id";
int num = "本次需要扣减的库存量";

// 需要给保护的数据生成一个唯一的:safeKey
String safeKey = "GoodsPO:"+商品id;

// 如下:根据 safe_key 去 t_concurrency_safe 表找这条需要保护的数据
ConcurrencySafePO po = (select * from t_concurrency_safe where safe_key = #{safe_key});

// 这条数据不存在,则创建,然后写到 t_concurrency_safe 表
if(po==null){
	po = new ConcurrencySafePO(#{safe_key});
    // 向 t_concurrency_safe 表写入一条数据
    insert into t_concurrency_safe (safe_key) values (#{safeKey});
}

// 下面执行扣减库存的操作,注意,如果用方案4,那么需要保护的数据的修改,均需要放在这个位置来保护,这块大家细品下
{
    //扣减库存前,查出商品库存
    GoodsPo beforeGoods = (select * from t_goods where goods_id = #{goodsId});
    
    //判断库存是否足够
    if(beforeGoods.num == 0){
        //库存不足,秒杀失败
        return;
    }

    // 执行扣减库存操作,条件中就只有goodsId,说明这个可能将库存扣成负数,出现超卖,继续向下看,后面的步骤将解决超卖
    update t_goods set num = num - ${购买的商品数量} where goods_id = #{goodsId}
}

//对 ConcurrencySafePO 执行乐观锁更新
int update = update t_concurrency_safe set version = version + 1 where id = #{po.id} and version = #{po.version}

// 若update==1,说明被保护的数据,期间没有发生变化
if(update == 1){
    //秒杀成功
}else{
    //说明被保护的数据,期间发生变化了,下面要抛出异常,让事务回滚
    throw new ConcurrencyFailException("系统繁忙,请重试");
}

如果是老的业务,涉及到大量代码,改造复杂,那么可以用此方案将业务代码包裹起来,便可防止并发修改导致数据不一致的问题。

源码

com.service.GoodsServiceImpl#placeOrder4

运行结果

===========================解决超卖,方案4 开始执行=======================================
模拟 100 人进行抢购
抢购结束啦............

抢购前,商品库存:10
抢购后,商品库存:0
下单成功人数:10
下单失败人数:90
===========================解决超卖,方案4 执行结束=======================================

到底选哪种方案?

这几种方案都可以解决超卖的问题,但是方案1最靠谱。

这里说下原因:团队中,涉及到很多人修改代码,那么问题就来了,可能修改库存的地方,有多个口子,那么此时,用其他方案就可能存在风险了,可能会出错。

如果能做到,收敛到一个口子中去修改数据,就是最终修改数据都是一个口子,那么上面的方法都可以,都可确保数据不会出问题,你们觉得呢?

总结

本文介绍了4种方案解决超卖问题,每种方案都有其使用场景,可能感觉有些方案很罕见,但是也许在日后某些场景下,你就会用到。

说了这么多方案,也算是开拓下大家解决问题的思路,有些问题,也许有更多方案,每种方案都有其存在的价值,抱着开放的心态,才能不断精进。

一起加油。

你可能感兴趣的:(解决超卖的4种方案,java,高并发)