问题
高并发场景下(比如每秒上千订单这种,虽然平时达到这种并发级别的概率比较小,但是还是有可能,比如节假日大促,或者单个商品秒杀活动等),如果用分布式锁来防止库存超卖,如何对分布式锁进行高并发优化来应对这个场景?
在做技术调研时,搜到的一篇文章给了思路,额,你们都是大佬,我来Follow一下。
库存超卖现象是怎么产生的
虽然是自营电商,并且主要以各类家电商品为主,但是整个商城(包括内部商城)依然有各种商品如各类家电、数码产品(与各手机厂商合作,如VIVO)、日用产品、食品饮料等等,在打折,满减等营销优惠场景下,并发量还是相对较高的。那么库存超卖是如何产生的?下面借用网上一张流程图加以说明
这个图,其实很清晰了,假设订单系统部署两台机器上,不同的用户都要同时买10台iphone,分别发了一个请求给订单系统。接着每个订单系统实例都去数据库里查了一下,当前iphone库存是12台。在不做防库存超卖情况下,两个用户都能下单!于是乎,每个订单系统实例都发送SQL到数据库里下单,然后扣减了10个库存,其中一个将库存从12台扣减为2台,另外一个将库存从2台扣减为-8台。现在完了,库存出现了负数!没有20台iphone发给两个用户啊!。以上场景就好比,大家都在抢一台2匹的COLMO高端空调,超卖情况下,公司亏损严重,程序员背锅。。。
用分布式锁如何解决库存超卖问题?
其实,库存超卖问题是老生常态的问题,也有很多种技术解决方案的,比如悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作,等等。但是如果在使用分布式锁的情况下(主要考虑到数据准确性),是如何解决库存超卖问题的?
分布式锁的实现原理:
同一个锁key,同一时间只能有一个客户端拿到锁,其他客户端会陷入无限的等待来尝试获取那个锁,只有获取到锁的客户端才能执行下面的业务逻辑。
从上面逻辑代码可以看出,只有一个订单系统实例可以成功加分布式锁,然后只有他一个实例可以查库存、判断库存是否充足、下单扣减库存,接着释放锁。释放锁之后,另外一个订单系统实例才能加锁,接着查库存,一下发现库存只有2台了,库存不足,无法购买,下单失败。不会将库存扣减为-8的。
有没有其他方案可以解决库存超卖问题?
当然有啊!比如悲观锁,分布式锁,乐观锁,队列串行化,异步队列分散,Redis原子操作,等等,很多方案,我们对库存超卖有自己的一整套优化机制。但是在原始的实际落地场景中,选用的方案就是分布式锁,并且迭代了好几个版本。因此,其他解决方案暂时并不考虑。
现在我们来看看,分布式锁的方案在高并发场景下有什么问题?问题很大啊!分布式锁一旦加了之后,对同一个商品的下单请求,会导致所有客户端都必须对同一个商品的库存锁key进行加锁。比如,对iphone这个商品的下单,都必对“iphone_stock”这个锁key来加锁。这样会导致对同一个商品的下单请求,就必须串行化,一个接一个的处理。假设加锁之后,释放锁之前,查库存 -> 创建订单 -> 扣减库存,这个过程性能很高吧,算他全过程20毫秒,这应该不错了。那么1秒是1000毫秒,只能容纳50个对这个商品的请求依次串行完成处理。
所以看到这里,大家起码也明白了,简单的使用分布式锁来处理库存超卖问题,存在缺陷。缺陷就是同一个商品多用户同时下单的时候,会基于分布式锁串行化处理,导致没法同时处理同一个商品的大量下单的请求。这种方案,要是应对那种低并发、无秒杀场景的普通小电商系统,可能还可以接受(事实上,公司落地的初始版本方案就是这种,具体为什么选这种方案。。。得问走了的大佬)。因为如果并发量很低,每秒就不到10个请求,没有瞬时高并发秒杀单个商品的场景的话,其实也很少会对同一个商品在一秒内瞬间下1000个订单,因为小电商系统没那场景。
如何对分布式锁进行高并发优化?
不对商品key加锁,无法保证数据准确性,会发生库存超卖事故;如果对key加锁,将导致串行化,并发量又上不去,怎么办?有没有一种“折中”的方案?有!受JDK分段锁如ConcurrentHashMap启发,可以考虑把数据分成很多个段,每个段是一个单独的锁,所以多个线程过来并发修改数据的时候,可以并发的修改不同段的数据。不至于说,同一时间只能有一个线程独占修改ConcurrentHashMap中的数据。另外,Java 8中新增了一个LongAdder类,也是针对Java 7以前的AtomicLong进行的优化,解决的是CAS类操作在高并发场景下,使用乐观锁思路,会导致大量线程长时间重复循环。LongAdder中也是采用了类似的分段CAS操作,失败则自动迁移到下一个分段进行CAS的思路。其实分布式锁的优化思路也是类似的。
其实这就是分段加锁。你想,假如你现在iphone有1000个库存,那么你完全可以给拆成20个库存段,要是你愿意,可以在数据库的表里建20个库存字段,比如stock_01,stock_02,类似这样的,也可以在redis之类的地方放20个库存key。
总之,就是把你的1000件库存给他拆开,每个库存段是50件库存,比如stock_01对应50件库存,stock_02对应50件库存。接着,每秒1000个请求过来了,此时其实可以是自己写一个简单的随机算法,每个请求都是随机在20个分段库存里,选择一个进行加锁。这样就好了,同时可以有最多20个下单请求一起执行,每个下单请求锁了一个库存分段,然后在业务逻辑里面,就对数据库或者是Redis中的那个分段库存进行操作即可,包括查库存 -> 判断库存是否充足 -> 扣减库存。
这相当于什么呢?相当于一个20毫秒,可以并发处理掉20个下单请求,那么1秒,也就可以依次处理掉20 * 50 = 1000个对iphone的下单请求了.
但是,一旦对某个数据做了分段处理之后,有几个坑大家一定要注意:
(1)如果某个下单请求,咔嚓加锁,然后发现这个分段库存里的库存不足了,此时咋办?这时你得自动释放锁,然后立马换下一个分段库存,再次尝试加锁后尝试处理。这个过程一定要实现,避免分段库存不足而又无限持有锁导致其他用户无限期阻塞。
(2)如果某个时刻,分段库存对于单个用户下单库存而言不足,但是总库存却大于所有请求下单库存量的和时,该如何处理这种有库存也无法下单的场景?(要解决这种场景问题,实现上比较麻烦),但是就秒杀场景而言,出现几率较少,因为这种对用户下单的数量有限制,如每个用户最多购买一件商品。
分布式锁并发优化方案有没有什么不足?
实现太复杂了,不方便,实现成本高!
- 首先,你得对一个数据分段存储,一个库存字段本来好好的,现在要分为20个分段库存字段;
- 其次,你在每次处理库存的时候,还得自己写挑选算法,挑选一个分段来处理;
- 最后,如果某个分段中的数据不足了,你还得自动切换到下一个分段数据去处理。 当然在这种场景下,也需要采用分库分表方案。