zoukankan      html  css  js  c++  java
  • 秒杀超卖

    最近在看秒杀相关的项目,针对防止库存超卖的问题,查阅了很多资料,其解决方案可以分为悲观锁、乐观锁、分布式锁、Redis原子操作、队列串行化等等,这里进行浅显的记录总结。

    首先我们来看下库存超卖问题是怎样产生的:

    //1.查询出商品库存信息

    select stock from t_goods where id=1;

    //2.根据商品信息生成订单

    insert into t_orders (id,goods_id) values (null,1);

    //3.修改商品库存

    update t_goods set stock=stock-1 where id=1;

    在高并发场景下,如果同时有两个线程a和b,同时查询到商品库存为1,他们都认为存库充足,于是开始下单减库存。如果线程a先完成减库存操作,库存为0,接着线程b也是减库存,于是库存就变成了-1,商品被超卖了。

    下面让我们来看看针对库存超卖问题的解决方案;

    解决方案一:悲观锁
    所谓悲观锁,即悲观的认为自己在操作数据库时,会大几率出现并发,于是在操作前会先进行加锁,操作完成后再释放锁。如果加锁失败说明该记录正在被修改,那么当前操作可以等待后尝试。

    以我们常用的MySQL为例,行锁、表锁、排他锁等都是悲观锁,为避免冲突,会在操作时先加锁,其他线程必须等待它的完成。

    这里我们通过使用select...for update语句,在查询商品表库存时将该条记录加锁,待下单减库存完成后,再释放锁。

    //0.开始事务

    begin;/begin work;/start transaction; (三者选一就可以)

    //1.查询出商品信息

    select stock from t_goods where id=1 for update;

    //2.根据商品信息生成订单

    insert into t_orders (id,goods_id) values (null,1);

    //3.修改商品stock减一

    update t_goods set stock=stock-1 where id=1;

    //4.提交事务

    commit;

    这样可以解决并发时库存超卖的问题,然而高并发时,所有的操作都被串行化了,效率很低,将严重影响系统的吞吐量。而且使用悲观锁还有可能造成死锁问题。

    解决方案二:乐观锁
    现在我们尝试下使用乐观锁,所谓乐观锁,是相对于悲观锁而言的,它假设数据一般情况下不会发生并发,因此不会对数据进行加锁,操作完成提交时才对数据是否冲突进行检测,如果发现冲突则返回错误。

    比较常见的实现方式是,在表中增加一个version字段,操作前先查询version信息,在数据提交时检查version字段是否被修改,如果没有被修改则进行提交,否则认为是过期数据。

    //1.查询出商品信息

    select stock, version from t_goods where id=1;

    //2.根据商品信息生成订单

    insert into t_orders (id,goods_id) values (null,1);

    //3.修改商品库存

    update t_goods set stock=stock-1, version = version+1 where id=1, version=version;

    这样,在并发时,如果线程a尝试修改商品库存时,发现版本号已经被线程b修改了,线程a执行update语句条件不满足便不再执行了,库存也不会被超卖。

    但是这种乐观锁的方式,在高并发时,只有一个线程能执行成功,会造成大量的失败,这给用户的体验显然是很不好的。

    这里我们可以减小锁的颗粒度,最大程度提升系统的吞吐量,提高并发能力:

    1

    2

    //修改商品库存时判断库存是否大于0

    update t_goods set stock=stock-1 where id=1 and stock>0;

    上面的update语句通过stock>0进行乐观锁的控制,在执行时,会在一次原子操作中查询stock的值,并扣减一。

    解决方案三:分布式锁
    除了在数据库层面加锁,我们还可以通过在内存中加锁,实现分布式锁。例如我们可以在Redis中设置一个锁,拿到锁的线程抢购成功,拿不到锁的抢购失败。

    Redis的setnx方法可以实现锁机制,key不存在时创建,并设置value,返回值为1;key存在时直接返回0。线程调用setnx方法成功返回1认为加锁成功,其他线程要等到当前线程业务操作完成释放锁后,才能再次调用setnx加锁成功。

    Long TIMEOUT_SECOUND = 120000L;

    Jedis client = jedisPool.getResource();

    //线程设置lock锁成功

    while(client.setnx("lock",String.valueOf(System.currentTimeMillis())) == 1){

    Long lockTime = Long.valueOf(client.get("lock"));

    //持有锁超时后自动释放锁

    if (lockTime!=null && System.currentTimeMillis() > lockTime+TIMEOUT_SECOUND){

    client.del("lock");

    }

    Thread.sleep(10000);

    }

    ......

    ......

    client.del("lock");

    解决方案四:Redis原子操作
    虽然通过以上方按可以防止库存超卖,但是高并发情况下对数据库进行频繁操作,会造成严重的性能问题。因此我们必须在前端对请求进行限制。

    我们可以在Redis中设置一个队列key为商品的id,队列的长度为商品库存量。每次请求到达时pop出一个元素,这样拿到元素的请求即认为秒杀成功,后续通过MQ发送消息异步完成数据库减库存操作。没有拿到元素的请求即认为秒杀失败。

    由于Redis是工作线程是单线程的,而list的pop操作是原子性的,因此并发的请求都被串行化了,库存就不会超卖了。

    //获取商品库存

    String token = redisTemplate.opsForList().leftPop(goodsStock);

    if(token == null){

    log.info(">>>商品已售空");

    return setResultError("亲,该秒杀已经售空,请下次再来!");

    }

    //异步发送MQ消息,执行数据库操作

    sendSecondKillMsg(goodsId, userId);

  • 相关阅读:
    hbase 由于zookeeper问题导致连接失败问题
    Python 判断文件/目录是否存在
    mysql5.7设置默认的字符集
    mysql 提示ssl问题
    Ubuntu 安装MySQL报共享库找不到
    hbase 监控指标项
    大量数据通过Phoenix插入到hbase报错记录(2)
    通过phoenix导入数据到hbase出错记录
    mysql5.7 之 sql_mode=only_full_group_by问题
    Hadoop 在启动或者停止的时候需要输入yes确认问题
  • 原文地址:https://www.cnblogs.com/matengfei123/p/12828341.html
Copyright © 2011-2022 走看看