扣减库存(高并发更新数据库都可使用)

对于“秒杀”活动,一般公司都是不允许商品超卖的,例如我司短信平台不允许客户超额消费。一旦超出即会造成损失。如果被恶意流量利用,则损失巨大。

  1. 扣减的方式
    为了不超卖,扣减常用以下三种方式:
  • 下单后扣减库存:这种扣减方式最简单,也最好理解,但是存在用户下单后不付款,特别是被恶意用户利用“秒杀器”大量抢购商品,但是不支付。如果是这种情况,那商家就无法达到真正目的,且用户无法得到对应商品。
  • 支付后扣减库存:在用户付款玩后再扣减库存,这样能解决上面方案中的“下单不支付”的情况。但是这种方式容易出现超卖的问题。因为,在瞬时流量高峰下,会有很多用户付款成功,但只有一小部分能真正抢到商品,大部分用户在付款后系统会提示“已售罄”或“活动结束”。
    注意:但是此种方式,可以通过上架补货完成业务。
  • 预扣减库存:用户下完订单后,系统会为其锁定库存一段时间,例如30分钟;在超过锁定时间后自动释放库存让其他用户抢购。当用户付款时,系统会校验库存是否在锁定有效期内,如果在有效期内,则可以进行支付;如果有效期已过,则重新锁定库存,若锁定失败则提醒库存不足。
    用户下单后,有些平台会有一个支付倒计时,例如12306,这也与预扣减方式相同。但此种方式也存在恶意用户占用有效时间的可能。
  1. 在高并发的情况下,如何扣减库存**
    在我们工作中根据实际业务去选择以上三种方式,目前最强用的就是第三种方式。
    对于上述第一种扣减方式,利用数据库的事务特性,可以保证订单和库存扣减数量的一致性。
  1. 先将商品数量查询出来
    例如:
select num from table where pro_id = #{pro_id}
  1. 更新库存
    用“库存数量”减去“购买的数量”,然后将结果值更新到数据库中。例如原本数量为10,用户买了2个,则库存为8
update table set num = 8 where pro_id = #{pro_id}

注意:为什么是更新“结果值”的操作,而不是直接更新“扣减”的操作。

因为,“扣减”操作不是幂等的,如果接口设计的不够完美,没有考虑幂等性,那么在由于网络原因或者其他原因造成重试之后,会出现重复“扣减”,导致“超卖”,甚至库存为负数的情况。

通过以上处理,基本可以保证下单扣减库存的准确性,但是对于“秒杀”,依然存在风险,例如两个用户同时抢购,都拿到了库存数量为10的商品,其中一个用户购买了5件商品,随后更新库存数量为5件;领一个用户请求购买了3件商品,随后更新库存数量为7件,则会出现并发更新数据不一致的情况。

所以在更新库存数量时,要将“当前库存数量”与“之前库存数量”进行比对,例如:

update table set num = #{new_num} where pro_id = #{pro_id} and num = #{old_num}

有了以上这种比对,在并发更新时,两个用户只有在更新提交前查询到库存为10的用户才能成功更新库存数量。

  1. 高并发的情况下,优化库存数量扣减
    通过流量的分层控制,可以管控大量的“读”请求。但是依然会有很大的流量进入真正的下单逻辑。对于这么大的流量,除去上述说的数据库隔离外,还需要进一步优化,否则数据库的读/写性能依然是系统的瓶颈。
    优化操作:
  1. 利用好缓存
    如果只是简单的一个扣减库存的操作,可以先将库存数量直接放到缓存中,然后利用分布式缓存,例如大名鼎鼎的Redis,去应对这种高并发情况下的系统挑战。
    注意:使用缓存也是存在一定风险的,比如:缓存节点出现了异常,库存如何处理?使用缓存不仅需要考虑分布式缓存高可用,还要考虑各种限流容错机制,以确保分布式缓存对外提供服务。
  2. 异步处理
    如果是复杂的扣减库存操作(比如涉及信息校验或牵连其他模块),则建议数据库直接进行数量的扣减,可以使用异步的方式来应对。
  • 在用户下单后,不立即生成订单,而是将所有订单依次放入队列。
  • 下单模块依据自身的处理速度,从队列中依次获取订单进行扣减操作。
  • 在订单生成后,用户即可进行支付操作。

这种方式是针对“秒杀”这种场景的,依据先到先得的原则来保证公平公正,所有用户都可以抢购,而且对于用户来说体验感也不会很差。

注意:对于用户来说只是在抢购时有一次提交抢购请求,之后就是等待程序处理进度。这个时间不可以太久,不管成功或者失败都要及时给用户反馈,要不然用户体验感极差。