今天和大家聊一个有趣的话题:每秒上千单场景下如何提高分布式锁的并发优化?背景介绍首先我们来看一下这道题的背景?前段时间有个朋友在外面面试,然后有一天他和我聊天:国内有个不错的电商,面试官问了他一个场景问题:下单的话,用分布式锁来防止超卖库存,但是是每秒几千单的高并发场景。如何针对高并发优化分布式锁来应对这种场景?他说当时没有回答,因为没做过,没有想法。其实听到这个面试题的时候,我也觉得有点意思,因为如果让我去面试应聘者,我应该给的范围更广一些。比如让面试的同学说一下电商高并发秒杀场景下超卖库存的解决方案,各种方案的优缺点和实践,然后再聊分布式锁的话题。因为解决库存超卖问题的技术有很多,比如悲观锁、分布式锁、乐观锁、队列序列化、Redis原子操作等等。但是由于面试官兄弟仅限于使用分布式锁来解决超卖库存,我想我只想问一个观点:高并发场景下如何优化分布式锁的并发性能。我觉得面试官提问的角度还是可以的。接受,因为在实际生产中,分布式锁保证了数据的准确性,但是其天生的并发能力有点弱。正好之前在自己项目的其他场景做过一个高并发场景的分布式锁优化方案,所以正好趁着这位朋友的面试题,给大家分享一下分布式锁的高并发优化思路。聊聊天。库存超卖现象是如何产生的?我们先来看看如果不使用分布式锁,所谓的超卖电商库存是什么意思?看下图:这张图其实很清晰。假设订单系统部署在两台机器上,不同用户想同时购买10部iPhone,分别向订单系统发送请求。然后每个订单系统实例去数据库查询,当前iphone库存为12台。两位大哥一看,乐了,12台的库存,比10台的采购量还多!于是乎,每个订单系统实例向数据库发送SQL下单,然后扣除10个库存,其中一个库存从12减到2,另一个库存从2减到-8塔。现在结束了,库存为负!泪,送两个用户的20部iphone都没有!这怎么能好。如何使用分布式锁解决库存超卖问题?我们如何使用分布式锁来解决库存超卖的问题呢?其实很简单。回想一下我们上次提到的分布式锁的实现原理:同一个锁key,同时只能有一个client拿到锁,其他client会陷入无限等待尝试获取锁。只有获取到锁的客户端才能执行下面的业务逻辑。代码大概就是上面那个,现在分析一下,为什么这样可以避免库存超卖?大家可以按照上面步骤的序号来阅读,马上就明白了。从上图可以看出,订单系统只有一个实例可以成功添加分布式锁,然后只有一个实例可以检查库存,判断库存是否充足,下单扣除库存,然后释放锁。释放锁后,可以锁定订单系统的另一个实例。然后查看库存,发现库存只有2台。库存不足无法采购,订单失败。不会将库存减少到-8。有没有其他办法解决超卖库存问题?当然有!比如悲观锁、分布式锁、乐观锁、队列序列化、异步队列分散、Redis原子操作等,对于很多解决方案,我们都有自己的一套超卖库存优化机制。但是之前说过,这篇文章讲的是分布式锁的并发优化,不是超卖库存的解决方案,只是一个业务场景。分布式锁方案在高并发场景下表现不错。下面我们来看看,分布式锁方案在高并发场景下有哪些问题?问题很大!兄弟,不知道你看不看。添加分布式锁后,同一商品的订单请求会导致所有客户端锁定同一商品的库存锁key。》这个lockkey是用来锁的,这样会导致同一个商品的订单请求,必须一个一个序列化处理,回去再反复看上面的图,应该能搞清楚这个问题假设加锁后,释放锁前,查看库存->创建订单->扣除库存,这个过程性能很高,整个过程耗时20毫秒,应该不错,那么1秒就是1000毫秒,只能容纳这个商品50个请求,按顺序完成处理,比如1秒有50个请求,都是iphone的订单,那么每个请求处理20毫秒,一个由一,最后1000毫秒刚好处理了50个请求,看下图加深感受,所以看到这里,大家至少明白单纯使用分布式锁来处理超卖的缺点了论。缺点是当多个用户同时下单同一商品时,会基于分布式锁串行处理,无法同时处理同一商品的大量订单。这种方案如果是处理普通的低并发、无秒杀场景的小型电商系统,或许还可以接受。因为如果并发很低,每秒请求不到10个。如果没有单品瞬间高并发秒杀,其实同一商品一秒下单1000单是很少见的,因为小电商系统没有那种场景。如何针对高并发优化分布式锁?好了,终于进入正题了,那么我们现在该怎么办呢?面试官说,我现在卡住了,超卖库存用分布式锁解决,一个iphone秒下单上千,怎么优化?现在按照刚才的计算,一秒只能处理iphone的50个订单。其实说起来很简单。相信很多人都看过java中ConcurrentHashMap的源码和底层原理。他们应该知道里面的核心思想,就是分段锁!数据分为很多段,每个段都是一个单独的锁,所以当多个线程来并发修改数据时,可以同时修改不同段的数据。并不是说只有一个线程可以同时独占修改ConcurrentHashMap中的数据。另外在Java8中新增了一个LongAdder类,同样针对Java7之前的AtomicLong进行了优化,解决了高并发场景下CAS操作使用乐观锁思想,会导致大量线程重复执行的问题循环很长时间。LongAdder也使用了类似的分段CAS操作,如果失败会自动迁移到下一个分段进行CAS。其实分布式锁的优化思路也是类似的。之前,我们在另一个业务场景中实现了这个方案并投入生产,并不是针对库存超卖的问题。但是超卖库存这个业务场景很好,也很容易理解,所以我们就拿这个场景来举例。我们看下图:其实这就是分段锁。你想,如果你现在有1000部iPhone库存,那么你可以将它们分成20个库存部分。如果你愿意,你可以在数据库表中创建20个stock字段,比如stock_01,stock_02,类似这样,也可以把20个库存key放在redis这样的地方。简而言之就是拆解你的1000件库存给他,每个库存段是50件库存,比如stock_01对应50件库存,stock_02对应50件库存。然后,每秒1000个请求来了,好!这时候你其实可以自己写一个简单的随机算法。每个请求在20个段位股票中随机抽取一个,选出一个进行锁定。答对了!这样就好了,最多同时执行20个订单请求。每个订单请求锁定一个库存分段,然后在业务逻辑中,对数据库或Redis中的分段库存进行操作。是的,包括查看库存->判断库存是否充足->扣减库存。这意味着什么?相当于20毫秒,可以并发处理20个订单请求,所以1秒内可以顺序处理iphone的20*50=1000个订单请求。一条数据切分后,有一个坑大家一定要注意:如果某个订单请求被点击锁定,然后发现切分的库存里面的库存不足,怎么办这次?这时候就得自动释放锁,然后马上换到下一个段库存,再尝试加锁,尝试处理。这个过程必须要实现。分布式锁并发优化方案有没有缺点?必有不足之处。最大的不足就是大家发现都没有,很不方便!实施起来太复杂了。首先,你必须分段存储一段数据。一个库存字段本来很好,现在需要分成20个分段库存字段;其次,每次处理库存时,都要自己写一个随机算法,随机选择一个Segment进行处理;最后,如果一个segment中的数据不足,就得自动切换到下一个segment数据去处理。这个过程是通过手工写代码实现的,还是比较费工夫和麻烦的。但是我们确实在一些业务场景中使用了分布式锁,那么我们就要优化锁并发,进一步使用分段锁的技术方案,效果当然非常好,并发性能可以提升几十倍。本次优化方案的后续改进以我们在本文中提到的超卖库存场景为例。这么玩下去,会让自己很痛苦的!同样,这里的超卖库存场景只是一个演示场景。后续我会单独说说高并发秒杀系统架构下的库存超卖的其他解决方案。
