美文网首页
关于库存

关于库存

作者: 小王ovo | 来源:发表于2023-06-14 11:29 被阅读0次

    库存的核心(sql)

    update product_skus set stock = stock - #{num} where id = id and stock >= num
    或者
    update product_skus set stock = stock - #{num} where (stock- #{num}) >= 0

    缺点

    虽然这句sql使用了乐观锁 stock >= num但是还是会有以下问题.(在mysql默认的rr隔离级别下,update语句会加锁,所以是串行执行的)。行级锁的原因存在性能瓶颈,高并发会出现请求堵塞超时问题。

    其他方案(行锁 for update)

    解决方案

    将库存放入redis 使用INCR(原子自增+1)DECR(原子-1) INCRBY num(可以自定义加减数量) DECRBY
    原子修改库存,伪代码如下.

    秒杀场景

    //判断用户是否已经购买
    boolean exist = redisClient.query(productId,userId);
    if(exist) {
      return -1;
    }
    //判断库存是否充足,如果扣减之后小于0则库存不足
    if(redisClient.incrby(productId, -1)<0) {
      return 0;
    }
    //添加购买记录
    redisClient.add(productId,userId);
    return 1;
    

    附带一个lua脚本

      StringBuilder lua = new StringBuilder();
      lua.append("if (redis.call('exists', KEYS[1]) == 1) then");
      lua.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
      lua.append("    if (stock == -1) then");
      lua.append("        return 1;");
      lua.append("    end;");
      lua.append("    if (stock > 0) then");
      lua.append("        redis.call('incrby', KEYS[1], -1);");
      lua.append("        return stock;");
      lua.append("    end;");
      lua.append("    return 0;");
      lua.append("end;");
      lua.append("return -1;");
    

    非秒杀场景

    //判断库存是否充足,如果扣减之后小于0则库存不足
    if(redisClient.decrby(productId, num)>=0) {
      //正常下单
    }else{
      //库存不足,将扣减库存恢复至 redis
      redisClient.incrby(productId, num)
    }
    

    但是这样写两条命令就没有原子性了,在并发情况下一个请求库存不足,还没有恢复,另一个请求本来可以扣减成功,由于第一个请求还没有恢复导致扣减失败.可以使用redis锁或者lua脚本的方式保证两条命令的原子性.

    订单取消、订单售后、取消支付等情况回滚库存

    这种情况下并发并不高,直接使用数据库悲观锁恢复即可.记得同步redis.

    管理后台调整库存,如何防止调整库存时,产生超卖

    当作一种特殊的下单处理即可.

    关于redis锁(保证加锁和设置失效时间的原子性)

    为了防止redis中没有加载库存,大量请求打到数据库上的情况
    这种方案下锁的粒度是sku(即单个商品).
    1.使用set的方式加锁

    String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
    if ("OK".equals(result)) {
        return true;
    }
    return false;
    
    lockKey:锁的标识
    requestId:请求id
    NX:只在键不存在时,才对键进行设置操作。
    PX:设置键的过期时间为 millisecond 毫秒。
    expireTime:过期时间
    
    1. 使用自旋锁提高用户请求的成功率
    try {
      Long start = System.currentTimeMillis();
      while(true) {
          String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
         if ("OK".equals(result)) {
            return true;
         }
         
         long time = System.currentTimeMillis() - start;
          if (time>=timeout) {
              return false;
          }
          try {
              Thread.sleep(50);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
     
    } finally{
        unlock(lockKey,requestId);
    }  
    return false;
    

    当然可以使用redission更好.

    写在最后

    1.扣减库存有下单减,支付减,建议下单减,支付减容易产生超卖;
    2.在取消订单,支付回调,后台变更库存等边界点容易产生库存数据不准,要注意优化;
    3.如果系统并发确实很高,可以考虑限流(令牌桶);

    库存.png

    相关文章

      网友评论

          本文标题:关于库存

          本文链接:https://www.haomeiwen.com/subject/dmwhydtx.html