高并发下超卖问题及如何解决

2020-02-14  本文已影响0人  尘世的鱼

一般电子商务网站会有团购,秒杀等活动,而这样的活动特点是请求量激增,数以万计的用户会抢购一个商品,这样会面临活动商品库存有限,高并发下如何控制库存不出现超卖的问题。

注意:

  1. 我们的数据存放在MySQL中
  2. 使用的语言是Java

为何会发生超卖

一般库存扣除的逻辑代码如下:

//remainder为剩余库存数量
int remainder=statement.query("select remainder from stock where stock_id='$STOCK_ID$'");

//amount为本次订单抢购数量
if(remainder>=amount!=0){
   statement.execute("update stock set remainder=remainder-amount where stock_id='$STOCK_ID$'")
}

防止超卖我们首先想到的是通过事务去解决这个问题:

startTransaction();//开启事务

try{
   //remainder为剩余库存数量
   int remainder=statement.query("select remainder from stock where   stock_id='$STOCK_ID$'");

   //amount为本次订单抢购数量
   if(remainder>=amount!=0){
       statement.execute("update stock set remainder=remainder-amount where stock_id='$STOCK_ID$'")
   } 
 }catch(Exception e){
    rollback();//回滚
}

commit();//提交事务

上面的事务下执行逻辑其实隐藏一个很大的漏洞-有可能库存会变为负数(即超卖),具体分析如下:

  1. 由于是高并发,假设有三个用户a,b,c同时抢购该物品,并进入到了这个事务中,这三个用户查到的库存数是一样的(MySQL rr级别下总是读取事务开始时的行数据)
  2. 然后进入到update,假设这三个用户同时进入update操作,这个时候由于 行级锁的排他性限制,MySQL会将update操作串行化
  3. 上面update执行完后,有可能会发生库存变为负数的情况(超卖)

怎么解决超卖

最简单的方式("锁方式")

针对上述超卖的情况我们可以通过更改下执行sql代码来实现:

startTransaction();//开启事务

try{
       statement.execute("update stock set remainder=remainder-amount where stock_id='$STOCK_ID$' and $remainder>=$amount")
   } 
 }catch(Exception e){
    rollback();//回滚
}

commit();//提交事务

上面的修改可以杜绝库存超卖的现象。注意以上在MySQL一致性非锁定读(rr隔离级别下)。

针对以上,我们可以换个思路,在库存数据结构中加入version字段来控制记录修改版本,也可以解决上述问题,如下:

startTransaction();//开启事务

try{
   //remainder为剩余库存数量
   int remainder=statement.query("select remainder,version from stock where   stock_id='$STOCK_ID$'");

   //amount为本次订单抢购数量
   if(remainder>=amount!=0){
       statement.execute("update stock set remainder=remainder-amount,version=version+1 where stock_id='$STOCK_ID$' and version=$version")
   } 
 }catch(Exception e){
    rollback();//回滚
}

commit();//提交事务

但是我们真的能这么做吗?我们的业务可是高并发,面对的是1万+TPS,那如果按上面继续执行会遇到什么问题?

很显然不能,在高并发下,会有很多这样的修改(update),每个请求都需要等待"锁",某些请求可能永远都获取不到锁,这种请求就会卡在那里,直到超时。同时,由于这种写请求很多,会造成大量的请求超时,连锁反应就是应用系统连接数被耗光,直至系统异常crash。即使重启系统,由于请求量大,系统也会立马挂掉。

高并发下如何解决超卖

引入缓存

主要思路是:

  1. 首先在团购秒杀开始前将需要的物品库存信息放入缓存中
  2. 使用锁来处理其并发请求
  3. 将缓存中的数据同步到数据库。

我们此处使用redis作为缓存。

应用操作redis减库存的大体思路为:

  1. 首先通过redis api监听相关物品的库存信息,在事务开启前保证该物品库存信息无人修改
  2. 获取现有库存信息,判断库存不为0并且当前库存量大于等于订单所需数量
  3. 满足上述2的话则进行扣除操作
  4. 如果在1的过程中有别人更新了该物品库存信息版本,则重试
  5. 知道库存为0或者剩余库存不满足当前订单扣除数量退出

具体代码如下:

 public void secondBuyProduct(Jedis jedis, String stockId, int orders) {
        //CAS重试
        while (true) {
            try {
                //监视key,如果在后续事务执行之前key的值被其他命令所改动,那么事务将被打断
                jedis.watch(stockId);

                int prdNum = Integer.parseInt(jedis.get(stockId));
                //判断库存是否满足订单数量要求
                if (prdNum > 0 && prdNum - orders >= 0)) {

                    Transaction transaction = jedis.multi();
                    //减库存并写入
                    transaction.set(stockId, String.valueOf(prdNum - orders));

                    List<Object> res = transaction.exec();
                    //事务提交后如果为null,说明key值在本次事务提交前已经被改变,本次事务不执行。
                    if (res != null && !res.isEmpty()) {
                        System.out.println("抢购成功!");
                        break;
                    }

                } else {
                    System.err.println("被抢光了!");
                    break;
                }
            } catch (Exception e) {
                System.err.println("抢购出错:" + e.toString());
                e.printStackTrace();
            } finally {
                jedis.unwatch();
            }
        }
    }
上一篇下一篇

猜你喜欢

热点阅读