前言
关于高并发系统中,当前比较热门的还是属于“秒杀”系统,前面章节在整理了“秒杀”系统的相关设计概念后,本章节,来讲解扣减库存相关的业务逻辑。
1 库存的那些事
一般电商网站中,购买流程一般都是这样的:商品列表页看重某个商品,然后进入详情页浏览商品详细信息,有购买意向后,单击“支付”按钮(或者将商品加入购物车,从购物车进行结算),网站弹出支付方式让用户进行选择支付方式,并进行支付。
对于这样的购买流程,用户在下单时,一般不需要关心库存的,只有没有库存了,用户才会感到没有库存了。但是在高并发场景中,这样的购买流程就会出现问题。例如,在“秒杀”活动中,用户会争抢库存,如果没有控制好,则会出现“超卖”的情况。会给公司造成损失。
(对于“秒杀”活动,一般公司是不允许商品“超卖”的,即【下单成功的数量】不能大于【商品库存数量】。一旦出现“超卖”,公司则会出现损失。如果被恶意流量利用,则损失可能是巨大的。)
1.1 扣减库存方式
库存对于电商平台来说,是一个重要的业务指标,所以在技术上需要合理设计扣减库存,不能出现“超卖”的情况。
一般扣减库存常有以下3种方式:
-
下单后扣减库存:用户在下单后就扣减库存,这种扣减方式最简单,也最好理解。但是问题是,可能有用户下单后不付款,特别是竞争对手利用“秒杀器”大量抢购商品,但是不支付。如果是这样,那商家就没有达到活动的真正目的,且真正想得到商品的用户也无法得到低价商品。
-
支付后扣减库存:在用户付完款后再扣减库存,这样可以解决用户“下单但不支付”的情况。但是这种方式也有问题:会发生【超卖】情况。因为,在瞬时洪峰流量下,会有很多用户付款成功,但是只有一小部分用户能真正抢到商品,大部分用户在付完款后系统会报“已售罄”或“活动结束”等。
(对于“支付后扣减库存”这种方式,当出现库存不足时,有些商家是可以后续再补货的)
-
预扣减库存:在用户下完订单后,系统会为其锁定库存一段时间,例如30分钟;在超过锁定时间后,会自动释放锁定的库存,让其他用户抢购。当用户付款时,系统会校验库存是否在锁定有效期内,如果在有效期内,则可以进行支付;如果锁定有效期已过,则重新锁定库存,若锁定失败则报“库存不足”的提醒。
(在用户下单后,有些平台会有一个支付时间的倒计时。例如12306网站,会有一个30分钟的有效支付时间,这种扣减库存方式相当于“预扣减库存”方式。)
1.2 在千万级流量的“秒杀”中,如何扣减库存?
上面了解到,有三种扣减库存方式:下单后扣减库存、支付后扣减库存 以及 预扣减库存。对于“秒杀”业务,应当如何选择扣减库存的方式呢?
由于在“秒杀”场景中,商品一般对用户具有很强的吸引力,所以,在这种场景中使用“下单后扣减库存”方式更为合适。
(在“秒杀”场景中,大部分用户抱着“抢到就是赚到”的想法,一般都会付款,但是如果真的有竞争对手恶意下单不付款,应该怎么办?前面在流量管控中已经说到,可以对请求日志进行实时分析,让风控系统选择出恶意用户,然后将其封停。)
因此,对于“下单后扣减库存”这种方式,利用数据库的事物特性,可以保证订单和库存扣减数量的一致性。
下面来分析该如何去做好“下单后扣减库存”,防止商品被“超卖”。
(1)将某个商品的库存数量查询出来:
select num from stock where prod_id = $prod_id
(2)更新库存。用【库存数量】减去【购买的商品数量】,然后将结果值更新到库存数据库中。例如,商品库存数量为10,之后用户购买了2件,则d得到的结果值是:10 - 2 = 8,只需要将 8 更新为最新的库存数量即可。
问题:为什么是更新“结果值”的操作,而不是直接更新“扣减”的操作呢?
回答:因为,“扣减”不是幂等的,如果接口设计的不够完善,没有考虑到幂等性,那么在由于网络原因或者其他原因重试后,会出现重复“扣减”,即会出现“超卖”,甚至库存为负值的情况。
通过上面两步的处理,基本可以保证下单扣减库存的准确性,但是对于“秒杀”这样的高并发场景来说这样还是有风险的。例如,在大流量、高并发下,有两个用户同时抢购,都拿到了库存数量为10的商品,其中一个用户购买了5件商品,随后更新库存数量为:10 - 5 = 5 件;接着另外一个请求购买了3件商品,随后更新库存数量为:10 - 3 = 7件。
(在高并发场景中,有可能出现并发更新数据不一致的情况,对于上图的两次请求,正常来讲应该卖出8件商品,但是现在却卖出了3件商品,因为update set动作覆盖了之前的数据。)
在更新库存数量时,需要将【当前库存数量】与【之前的库存数量】进行对比:
update stock set num = $new_num where prod_id = $prod_id and num = $old num
有了这种对比,在并发更新时,用户A和用户B只有在更新提交前查询到的库存数量为10,才能成功更新库存数量。
但是,在这种并发场景中,如果没有进行对比,则会出现以下问题:
-
开始库存数量为10,第一个用户更新库存数量是成功的。
-
在第一个用户更新完库存数量后,库存数量变成了5,所以,这时另一个请求是不能扣减成功的,在代码中会提示更新失败或者提示用户扣减库存失败的提示。
1.3 在千万级流量的“秒杀”中,优化库存数量扣减方案
在“秒杀”场景中,通过流量分层控制可以分层管控大量的“读”请求。但是,依然会有很大的流量进入真正的下单罗技。对于这么大的的流量,除了前面说的数据库个例外,还需要进一步优化库存数量,否则数据库读/写依然是系统的瓶颈。
(1)利用好缓存。
在“秒杀”场景中,如果只是一个扣减库存数量这样简单的流程,则可以先将库存数量直接放在缓存中,然后利用分布式缓存(如Redis)去应对这种瞬时流量洪峰下的系统挑战。
ps:使用缓存是存在一定风险的,例如,缓存节点出现异常,那库存数量应该怎么计算?使用缓存不仅要考虑分布式缓存高可用,还要考虑各种限流容错机制,以确保分布式缓存对外提供服务。
(2)异步处理。
如果是复杂的扣减库存(如涉及商品信息本身或者牵连其他系统),则建议使用数据库进行库存数量的扣减,可以使用异步的方式来应对这种高并发的库存数量更新。
-
在用户下单时,不like生成订单,而是将所有订单依次放入队列。
-
下单模块依据自身处理速度,从队列中依次获取订单进行“下单扣减库存”的操作。
-
在订单生成成功后,用户即可进行支付操作了。
这种方式是针对“秒杀”场景的,依据“先到先得”的原则来保证公平公正,所有用户都可以抢购,然后等待订单处理,最后生成订单(如果库存不足,则生成订单失败)。这样的逻辑,对于用户来说体验感不是很差。具体排队逻辑如图:
(对于用户来说,只需要在商品活动详情页中提交一次抢购请求,之后就是等待系统处理进度。当然,这个等待时间是要设计的,不要让用户等待太久的时间,不管成功或者失败。)
上面介绍了千万级流量“秒杀”系统的基本架构、“秒杀”系统的设计原则、如何做动静分离方案和流量控制,以及扣减库存方案的内容。这些都是设计高并发“秒杀”系统必须要考虑的。
“秒杀”系统的流程并不复杂,只是一个“下单扣减库存”的动作,但是由于其具有独特的业务特点,所以在进行系统设计时千万不能大意。对于瞬时流量洪峰的高并发“秒杀”系统,我们可以总结一下:
(1)数据的静态化技术,用来应对高并发读的请求。
-
各层级缓存的处理(即多级缓存的技术)。
-
分布式缓存技术。
(2)负载均衡反向代理技术。
-
LVS。
-
Nginx。
(3)异步处理技术。
-
消息队列技术。
-
排队系统技术。
(4)系统架构设计技术。
-
系统模块化划分。
-
微服务架构思想。
(5)系统监控技术。
-
日志监控。
-
服务监控。
本章节主要讲解了库存扣减相关设计经验,在面对库存扣减这一业务,不能大意,需要认真进行系统设计、业务设计,选择合理的方案来解决库存扣减的场景。以及在最后进行了一个简单的前面章节的统一汇总,希望小伙伴们能够及时回顾前面章节内容。
下一章节会针对系统交互过程中的服务通信设计进行讲解。希望大家能够分享公众号给身边的小伙伴,大家一起来学习