再有人问你分布式锁,就把这个丢给他!

做者介绍node

中华石杉,十余年BAT架构经验倾囊相授。我的微信公众号:石杉的架构笔记(ID:shishan100)。面试

如今面试都会聊聊分布式系统,一般面试官都会从服务框架(Spring Cloud、Dubbo),一路聊到分布式事务、分布式锁、ZooKeeper等知识。今天就来聊聊分布式锁这块的知识,先具体的来看看Redis分布式锁的实现原理。redis

若是在公司里落地生产环境用分布式锁的时候,必定是会用开源类库的,好比Redis分布式锁,通常就是用Redisson框架就行了,很是的简便易用。感兴趣能够去Redisson官网看看如何在项目中引入Redisson的依赖,而后基于Redis实现分布式锁的加锁与释放锁。算法

一段简单的使用代码片断,先直观的感觉一下:数据库

再有人问你分布式锁,就把这个丢给他!

是否是感受简单的不行!此外,还支持Redis单实例、Redis哨兵、Redis Cluster、redis master-slave等各类部署架构,均可以完美实现。微信

1、Redisson实现Redis分布式锁的底层原理数据结构

如今经过一张手绘图,说说Redisson这个开源框架对Redis分布式锁的实现原理。架构

再有人问你分布式锁,就把这个丢给他!

一、加锁机制并发

看上面那张图,如今某个客户端要加锁。若是该客户端面对的是一个Redis Cluster集群,他首先会根据Hash节点选择一台机器。框架

注:仅仅只是选择一台机器!而后发送一段Lua脚本到Redis上,那段Lua脚本以下所示:

再有人问你分布式锁,就把这个丢给他!

为啥要用Lua脚本呢?由于一大坨复杂的业务逻辑,能够经过封装在Lua脚本中发送给Redis,保证这段复杂业务逻辑执行的原子性。

那么,这段Lua脚本是什么意思呢?这里KEYS[1]表明的是你加锁的那个Key,好比说:RLock lock = redisson.getLock("myLock");这里你本身设置了加锁的那个锁Key就是“myLock”。

  • ARGV[1]表明的就是锁Key的默认生存时间,默认30秒。

  • ARGV[2]表明的是加锁的客户端的ID,相似于下面这样的:8743c9c0-0795-4907-87fd-6c719a6b4586:1。

第一段if判断语句,就是用“exists myLock”命令判断一下,若是你要加锁的那个锁Key不存在的话,你就进行加锁。如何加锁呢?很简单,用下面的命令:hset myLock。

8743c9c0-0795-4907-87fd-6c719a6b4586:11,经过这个命令设置一个Hash数据结构,这行命令执行后,会出现一个相似下面的数据结构:

再有人问你分布式锁,就把这个丢给他!

上述内容就表明“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端,已经对“myLock”这个锁Key完成了加锁。

接着会执行“pexpiremyLock 30000”命令,设置myLock这个锁Key的生存时间是30秒,加锁完成。

二、锁互斥机制

这个时候,若是客户端2来尝试加锁,执行了一样的一段Lua脚本,会怎样?

第一个if判断会执行“exists myLock”,发现myLock这个锁Key已经存在了。

第二个if判断,判断myLock锁Key的Hash数据结构中,是否包含客户端2的ID,可是明显不是的,由于那里包含的是客户端1的ID。

因此,客户端2会获取到pttl myLock返回的一个数字,这个数字表明了myLock这个锁Key的剩余生存时间。好比还剩15000毫秒的生存时间。此时客户端2会进入一个while循环,不停的尝试加锁。

三、watch dog自动延期机制

客户端1加锁的锁Key默认生存时间才30秒,若是超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

只要客户端1加锁成功,就会启动一个watchdog看门狗,这个后台线程,会每隔10秒检查一下,若是客户端1还持有锁Key,就会不断的延长锁Key的生存时间。

四、可重入加锁机制

那若是客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?以下代码:

再有人问你分布式锁,就把这个丢给他!

分析一下上面那段Lua脚本。第一个if判断确定不成立,“exists myLock”会显示锁Key已经存在了。

第二个if判断会成立,由于myLock的Hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”。

此时就会执行可重入加锁的逻辑,incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:11,经过这个命令,对客户端1的加锁次数,累加1。

此时myLock数据结构变为下面这样:

再有人问你分布式锁,就把这个丢给他!

myLock的Hash数据结构中的那个客户端ID,就对应着加锁的次数。

五、释放锁机制

若是执行lock.unlock,就能够释放分布式锁,此时的业务逻辑也是很是简单的。就是每次都对myLock数据结构中的那个加锁次数减1。

若是发现加锁次数是0了,说明这个客户端已经再也不持有锁了,此时就会用:“del myLock”命令,从Redis里删除这个Key。

而另外的客户端2就能够尝试完成加锁了。这就是所谓的分布式锁的开源Redisson框架的实现机制。

通常咱们在生产系统中,能够用Redisson框架提供的这个类库来基于Redis进行分布式锁的加锁与释放锁。

六、上述Redis分布式锁的缺点

上面那种方案最大的问题,就是若是你对某个Redis Master实例,写入了myLock这种锁Key的Value,此时会异步复制给对应的Master Slave实例。

可是这个过程当中一旦发生Redis Master宕机,主备切换,Redis Slave变为了Redis Master。

会致使客户端2尝试加锁时,在新的Redis Master上完成加锁,客户端1也觉得本身成功加锁。

此时就会致使多个客户端对一个分布式锁完成了加锁。这时系统在业务语义上必定会出现问题,致使各类脏数据的产生。

因此这个就是Redis Cluster,或者是redis master-slave架构的主从异步复制致使的Redis分布式锁的最大缺陷:在Redis Master实例宕机的时候,可能致使多个客户端同时完成加锁。

2、七张图完全讲清楚ZooKeeper分布式锁的实现原理

下面再聊一下ZooKeeper实现分布式锁的原理。同理,我是直接基于比较经常使用的Curator这个开源框架,聊一下这个框架对ZooKeeper(如下简称ZK)分布式锁的实现。

通常除了大公司是自行封装分布式锁框架以外,建议你们用这些开源框架封装好的分布式锁实现,这是一个比较快捷省事的方式。

ZooKeeper分布式锁机制

看看多客户端获取及释放ZK分布式锁的整个流程及背后的原理。首先看看下图,若是如今有两个客户端一块儿要争抢ZK上的一把分布式锁,会是个什么场景?

再有人问你分布式锁,就把这个丢给他!

若是你们对ZK还不太了解的话,建议先自行百度一下,简单了解点基本概念,好比ZK有哪些节点类型等等。

参见上图。ZK里有一把锁,这个锁就是ZK上的一个节点。两个客户端都要来获取这个锁,具体是怎么来获取呢?

假设客户端A抢先一步,对ZK发起了加分布式锁的请求,这个加锁请求是用到了ZK中的一个特殊的概念,叫作“临时顺序节点”。简单来讲,就是直接在"my_lock"这个锁节点下,建立一个顺序节点,这个顺序节点有ZK内部自行维护的一个节点序号。

  • 好比第一个客户端来搞一个顺序节点,ZK内部会给起个名字叫作:xxx-000001。

  • 而后第二个客户端来搞一个顺序节点,ZK可能会起个名字叫作:xxx-000002。

  • 注意,最后一个数字都是依次递增的,从1开始逐次递增。ZK会维护这个顺序。

因此这个时候,假如说客户端A先发起请求,就会搞出来一个顺序节点,你们看下图,Curator框架大概会弄成以下的样子:

再有人问你分布式锁,就把这个丢给他!

客户端A发起一个加锁请求,先在要加锁的node下搞一个临时顺序节点,这列长名字都是Curator框架本身生成出来的。

而后,那个最后一个数字是"1"。由于客户端A是第一个发起请求的,因此给他搞出来的顺序节点的序号是"1"。

接着客户端A建立完一个顺序节点。还没完,他会查一下"my_lock"这个锁节点下的全部子节点,而且这些子节点是按照序号排序的,这个时候他大概会拿到这么一个集合:

再有人问你分布式锁,就把这个丢给他!

接着客户端A会走一个关键性的判断:这个集合里建立的顺序节点,是否排在首位?

若是是的话,就能够加锁,由于明明我就是第一个来建立顺序节点的人,因此我就是第一个尝试加分布式锁的人啊!

加锁成功!看下图,再来直观的感觉一下整个过程:

再有人问你分布式锁,就把这个丢给他!

接着假如说,客户端A都加完锁了,客户端B过来想要加锁了,这个时候他会干同样的事儿:先是在"my_lock"这个锁节点下建立一个临时顺序节点,此时名字会变成相似于:

再有人问你分布式锁,就把这个丢给他!

下图:

再有人问你分布式锁,就把这个丢给他!

客户端B由于是第二个来建立顺序节点的,因此ZK内部会维护序号为"2"。

接着客户端B会走加锁判断逻辑,查询"my_lock"锁节点下的全部子节点,按序号顺序排列,此时他看到的相似于:

再有人问你分布式锁,就把这个丢给他!

同时检查本身建立的顺序节点,是否是集合中的第一个?明显不是啊,此时第一个是客户端A建立的那个顺序节点,序号为"01"的那个。因此加锁失败!

加锁失败了之后,客户端B就会经过ZK的API对他的顺序节点的上一个顺序节点加一个监听器。ZK自然就能够实现对某个节点的监听。

若是你们还不知道ZK的基本用法,能够百度查阅,很是的简单。客户端B的顺序节点是:

再有人问你分布式锁,就把这个丢给他!

他的上一个顺序节点,不就是下面这个吗?

再有人问你分布式锁,就把这个丢给他!

即客户端A建立的那个顺序节点!因此,客户端B会对:

再有人问你分布式锁,就把这个丢给他!

这个节点加一个监听器,监听这个节点是否被删除等变化!你们看下图:

再有人问你分布式锁,就把这个丢给他!

接着,客户端A加锁以后,可能处理了一些代码逻辑,而后就会释放锁。那么,释放锁是个什么过程呢?

其实就是把本身在ZK里建立的那个顺序节点,也就是:

再有人问你分布式锁,就把这个丢给他!

这个节点删除。删除了那个节点以后,ZK会负责通知监听这个节点的监听器,也就是客户端B以前加的那个监听器,说:你监听的那个节点被删除了,有人释放了锁。

再有人问你分布式锁,就把这个丢给他!

此时客户端B的监听器感知到了上一个顺序节点被删除,也就是排在他以前的某个客户端释放了锁。

此时,就会通知客户端B从新尝试去获取锁,也就是获取"my_lock"节点下的子节点集合,此时为:

再有人问你分布式锁,就把这个丢给他!

集合里此时只有客户端B建立的惟一的一个顺序节点了!而后呢,客户端B判断本身竟然是集合中的第一个顺序节点,Bingo!能够加锁了!直接完成加锁,运行后续的业务代码便可,运行完了以后再次释放锁。

再有人问你分布式锁,就把这个丢给他!

其实若是有客户端C、客户端D等N个客户端争抢一个ZK分布式锁,原理都是相似的:

  • 你们都是上来直接建立一个锁节点下的一个接一个的临时顺序节点。

  • 若是本身不是第一个节点,就对本身上一个节点加监听器。

  • 只要上一个节点释放锁,本身就排到前面去了,至关因而一个排队机制。

  • 并且用临时顺序节点的另一个用意就是,若是某个客户端建立临时顺序节点以后,不当心本身宕机了也不要紧,ZK感知到那个客户端宕机,会自动删除对应的临时顺序节点,至关于自动释放锁,或者是自动取消本身的排队。

最后,我们来看下用Curator框架进行加锁和释放锁的一个过程:

再有人问你分布式锁,就把这个丢给他!

其实用开源框架就是方便。这个Curator框架的ZK分布式锁的加锁和释放锁的实现原理,就是上面咱们说的那样子。

可是若是你要手动实现一套那个代码的话,要考虑到各类细节,异常处理等等。因此你们若是考虑用ZK分布式锁,能够参考下本文的思路。

3、每秒上千订单场景下的分布式锁高并发优化实践

接着聊一个有意思的话题:每秒上千订单场景下,如何对分布式锁的并发能力进行优化?

首先,咱们一块儿来看看这个问题的背景。前段时间有个朋友在外面面试,而后有一天找我聊说:有一个国内不错的电商公司,面试官给他出了一个场景题:

假以下单时,用分布式锁来防止库存超卖,可是是每秒上千订单的高并发场景,如何对分布式锁进行高并发优化来应对这个场景?

他说他当时没答上来,由于没作过没什么思路。其实我当时听到这个面试题内心也以为有点意思,由于若是是我来面试候选人的话,给的范围会更大一些。好比,让面试的同窗聊一聊电商高并发秒杀场景下的库存超卖解决方案,各类方案的优缺点以及实践,进而聊到分布式锁这个话题。

由于库存超卖问题是有不少种技术解决方案的,好比悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操做,等等吧。可是既然那个面试官兄弟限定死了用分布式锁来解决库存超卖,我估计就是想问一个点:在高并发场景下如何优化分布式锁的并发性能。

面试官提问的角度仍是能够接受的,由于在实际落地生产的时候,分布式锁这个东西保证了数据的准确性,可是他自然并发能力有点弱。

恰好我以前在本身项目的其余场景下,确实是作太高并发场景下的分布式锁优化方案,所以正好是借着这个朋友的面试题,把分布式锁的高并发优化思路,给你们来聊一聊。

一、库存超卖现象是怎么产生的?

先来看看若是不用分布式锁,所谓的电商库存超卖是啥意思?你们看下图:

再有人问你分布式锁,就把这个丢给他!

这个图其实很清晰了,假设订单系统部署在两台机器上,不一样的用户都要同时买10台iPhone,分别发了一个请求给订单系统。

接着每一个订单系统实例都去数据库里查了一下,当前iPhone库存是12台,大于了要买的10台数量。

因而每一个订单系统实例都发送SQL到数据库里下单,而后扣减了10个库存,其中一个将库存从12台扣减为2台,另一个将库存从2台扣减为-8台。

如今库存出现了负数!没有20台iPhone发给两个用户啊!怎么办?

二、用分布式锁如何解决库存超卖问题?

咱们用分布式锁如何解决库存超卖问题呢?回忆一下上次咱们说的那个分布式锁的实现原理:

同一个锁Key,同一时间只能有一个客户端拿到锁,其余客户端会陷入无限的等待来尝试获取那个锁,只有获取到锁的客户端才能执行下面的业务逻辑。

再有人问你分布式锁,就把这个丢给他!

代码如上图,分析一下为何这样能够避免库存超卖?

再有人问你分布式锁,就把这个丢给他!

你们能够顺着上面的那个步骤序号看一遍,立刻就明白了。

从上图能够看到,只有一个订单系统实例能够成功加分布式锁,而后只有他一个实例能够查库存、判断库存是否充足、下单扣减库存,接着释放锁。释放锁以后,另一个订单系统实例才能加锁,接着查库存,一下发现库存只有2台了,库存不足,没法购买,下单失败。不会将库存扣减为-8的。

三、有没其余方案解决库存超卖问题?

固然有!好比悲观锁,分布式锁,乐观锁,队列串行化,异步队列分散,Redis原子操做,等等,不少方案,咱们对库存超卖有本身的一整套优化机制。可是前面说过,这篇文章就聊一个分布式锁的并发优化,不是聊库存超卖的解决方案,因此库存超卖只是一个业务场景而已。

四、分布式锁的方案在高并发场景下

如今咱们来看看,分布式锁的方案在高并发场景下有什么问题?分布式锁一旦加了以后,对同一个商品的下单请求,会致使全部客户端都必须对同一个商品的库存锁Key进行加锁。

好比,对iPhone这个商品的下单,都必对“iphone_stock”这个锁Key来加锁。这样会致使对同一个商品的下单请求,就必须串行化,一个接一个的处理。你们再回去对照上面的图反复看一下,应该能想明白这个问题。

假设加锁以后,释放锁以前,查库存→建立订单→扣减库存,这个过程性能很高吧,算他全过程20毫秒,这应该不错了。那么1秒是1000毫秒,只能容纳50个对这个商品的请求依次串行完成处理。如一秒钟50个请求,都是对iPhone下单的,那么每一个请求处理20毫秒,逐个来,最后1000毫秒正好处理完50个请求。

你们看下图,加深印象。

再有人问你分布式锁,就把这个丢给他!

因此看到这里,你们起码也明白了,简单的使用分布式锁来处理库存超卖问题,存在什么缺陷。

同一商品多用户同时下单时,会基于分布式锁串行化处理,致使无法同时处理同一个商品的大量下单的请求。这种方案应对那种低并发、无秒杀场景的普通小电商系统,可能还能够接受。

由于若是并发量很低,每秒就不到10个请求,没有瞬时高并发秒杀单个商品的场景的话,其实也不多会对同一个商品在1秒内瞬间下1000个订单,由于小电商系统没那场景。

五、如何对分布式锁进行高并发优化?

那么如今怎么办呢?面试官说,我如今就卡死,库存超卖就是用分布式锁来解决,并且一秒对一个iPhone下上千订单,怎么优化?

如今按照刚才的计算,你1秒钟只能处理针对iPhone的50个订单。其实说出来也很简单,相信不少人看过Java里的Concurrent Hash Map的源码和底层原理,应该知道里面的核心思路,就是分段加锁!

把数据分红不少个段,每一个段是一个单独的锁,因此多个线程过来并发修改数据的时候,能够并发的修改不一样段的数据。不至于说,同一时间只能有一个线程独占修改Concurrent Hash Map中的数据。

另外,Java8中新增了一个Long Adder类,也是针对Java7之前的Atomic Long进行的优化,解决的是CAS类操做在高并发场景下,使用乐观锁思路,会致使大量线程长时间重复循环。Long Adder中也采用了相似的分段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的下单请求了。

一旦对某个数据作了分段处理以后,有一个坑你们必定要注意:就是若是某个下单请求,咔嚓加锁,而后发现这个分段库存里的库存不足了。这时你得自动释放锁,而后立马换下一个分段库存,再次尝试加锁后尝试处理。这个过程必定要实现。

六、分布式锁并发优化方案有什么不足?

最大的不足是很不方便,实现太复杂:

  • 首先,你得对一个数据分段存储,一个库存字段原本好好的,如今要分为20个库存字段。

  • 其次,你在每次处理库存的时候,还得本身写随机算法,随机挑选一个分段来处理。

  • 最后,若是某个分段中的数据不足了,你还得自动切换到下一个分段数据去处理。

这个过程都是要手动写代码实现的,仍是有点工做量。不过咱们确实在一些业务场景里,由于用到了分布式锁,而后又必需要进行锁并发的优化,又进一步用到了分段加锁的技术方案,效果固然是很好的了,一会儿并发性能能够增加几十倍。

该优化方案的后续改进:以咱们本文所说的库存超卖场景为例,你要是这么玩,会把本身搞的很痛苦!再次强调,咱们这里的库存超卖场景,仅仅只是做为演示场景而已。

相关文章
相关标签/搜索