减库存

类型

  • 下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。
    • 容易恶意下单
  • 付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。
    • 但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
    • 而且涉及支付业务,容易造成超卖
  • 预扣库存,买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。
    • 在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。
    • 如果被长时间恶意下单,没啥用

缺点和解决方案

  • 下单减库存:容易被恶意下单
  • 付款减库存:涉及支付等服务,容易造成超卖,用户无法付款,体验差
  • 预扣库存:如果被长时间恶意下单,也有一定的风险

场景

  • 普通用预扣库存,可以结合安全和反作弊的措施来制止恶意下单的操作
    • 给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存)、给某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复下单不付款的操作进行次数限制等
  • 秒杀场景可以使用下单减库存的操作
    • 由于并发量高,用性能高的优先
    • 秒杀场合,一般被恶意下单的几率较小
    • 保证数据一致性,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数
      • 是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚
      • 直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错
      • 使用 CASE WHEN 判断语句,例如这样的 SQL 语句:UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
    • 操作:先创建订单但是先不生效,然后减库存,如果减库存成功后再生效订单,否则订单不生效

待切分

简单逻辑

  • 判断当前用户是否抢购果当前商品

    • 判断商品是否符合要求 代码一
    • 扣减库存 代码二
    • 判断扣减库存是否成功
      • 成功
        • 双重检查,可以使用分布式锁进行包裹
          • 当前用是否抢购过当前商品 && 库存减一操作
      • 失败
  • 代码一

    1. select
    2. A.*,
    3. B.name as itemName,
    4. (
    5. -- 处于秒杀时间内且数量大于1
    6. case when (now() BETWEEN A.start_time AND A.end_time AND A.total > 0)
    7. then 1
    8. else 0
    9. end
    10. )
    11. from product_kill as A left join product B on A.id = B.item_id
    12. where A.is_active
    13. [and A.]
  • 代码二

    1. update product_kill
    2. set total = total - 1
    3. where id = <id>
    4. -- 商品必须大于0,防止超卖
    5. and total > 0;

extra

原因

  • 事务并行,get then update

方案

db

  1. 串行化 no
  2. 修改记录对记录加锁 no 会死锁
  3. 乐观锁,使用 version
    1. get 获得 before_version
    2. set if current_version = before_version

redis

  • 库存保存在 redis 中

    方式一

  • redis watch使命令进入队列 + 乐观锁机制

  • exec() 时会比对当前 versionwatch 时的 version,相同才执行
  1. 两个值,product_countkill_success_user_list
    1. MyJedisPool.execute(jedis -> {
    2. jedis.watch("product_count", "kill_success_user_list");
    3. int num = Integer.parseInt(jedis.get("product_count"));
    4. System.out.println(num);
    5. if (num > 0) {
    6. Transaction transaction = jedis.multi();
    7. transaction.decr("product_count");
    8. transaction.rpush("kill_success_user_list", idsList.pop());
    9. transaction.exec();
    10. }
    11. });

    方式二

  • 我 jio 得用 lua 判断 decrement 后 库存 > 0
    • 判断为 true 则 然后发送 mq