全国服务热线:18980020603 成都热线:028-86633922
新闻中心网站专题联系我们
行业新闻 建站经验 网站建设资讯 手机网站资讯 微信网站建设资讯 APP开发资讯 商城网站资讯

成都网站建设:Zookeeper的分布式锁怎么实现

发布人:桔子科技    发布时间:2017-09-22 00:21:31    分享到:
成都网站建设:工作这么多年了,从来没想过分享过啥,从今儿开始,把自己做过的东西分享出来和大家一起探讨,从简单一点点来吧。
\

现阶段工作中,我们服务端集群因要应对比较复杂的业务场景往往面临着经常性的资源、数据的共享、竞争及调度等问题。特别是对于我们这种有状态的服务来说,如何合理的协调各个服务之间所承载的资源,并且在服务端集群可用的前提下增加或减少服务数量而尽量保证对原有客户无影响则成为比较重要的问题。而对于我们来说,解决此类问题的方式大多有两类,一是尽量避免资源的征用,比如说使用特定算法(哈希环、轮询等),而另外一种是在需要协调工作的一组服务器所共享的资源上加锁。对于我们来说,一个现实是多数直接处理客户请求的服务器都是“有限状态机”模式工作,那么在调度层进行客户请求路由时,其应该也是具有统一状态的,这样比如说当一台或多台业务服务脱离集群或因整体负载达到上限就需要统一重新协调其状态,使之在调度者协调完毕后再次统一。而另外一种则是保证共享资源在竞争时(这里也就是进程)的一致性。由于业务的复杂性,客户数据往往根据需要存储在多个不同三方数据中间件中,典型的比如说对于需要根据不同需求做多角度存取的数据我会把它存在Redis集群中,并使用多个数据结构对其进行描述(hash用来快速定位,有序集合进行快速获取子集等操作)用以加快速度,而对于结构不是那么统一的“脏”数据,通常存储在文档形数据库中,比如说mongo。所以对于以上特别是多数据源同时操作时,锁的代价很大但有时候也是必要的,为解决部分上述问题搞了一套分布式锁。

对于进程锁来说,要维持各个持有锁节点的统一性并对它们进行统一调度要完全依赖于网络和一组调度服务,同时锁的状态也必要以多个副本的形式保存在这组服务中,并且还要考虑到容灾、锁的公平性,网络的不可预知性,协议设计、通讯方式避免过多不必要的数据交互以及herd effect等,若所有以上考虑到的问题都自己实现,那实现该锁将遥遥无期,这里采用Zookeeper的ZAB协议解决如上问题。关于该协议的工作原理,能够给我们提供的以上问题的解决方案和为了这些的代价所带来的缺陷在以后会进行分享讨论,这里仅仅贴出我实现的分布式锁的部分代码和思路,大家一起探讨:

首先分布式锁的持有者是进程,换句话说分布式独享锁是进程独享的。对于一个已经获得锁的进程来说,其锁覆盖的资源是所有线程共享的,若想对资源进行进一步划分,使用线程锁或其他方式但是不该分布式锁的事儿。所以当一个进程获取到了锁,可以说是所有线程都对这部分资源有了访问权限,而当一把锁被该进程释放,这意味着所有线程都完成了对该资源的存取。这里需要处理多线程的并发和一致性问题。另外它应该是一把公平锁,也就是说对一个资源的排队访问应该按进程到来的先后顺序进行调度(ZAB实现),它也应该是一把乐观锁,当某个进程长期持有该锁的时候,其他进程应该有自己的超时策略或者可以被中断甚至可以尝试与持有锁的进程通讯进行状态查阅等等。最后,该锁还应该是一把可重入锁,而重入在业务角度上讲可能会有另外的含义,而从技术角度上我仅仅是想把其与ReentrantLock的语义相统一。总结其定义:进程独享的,线程共享的,可重入的、乐观的、公平锁。首先定义接口对锁的基本功能进行描述:

 

两组锁的定义:独享锁和读写锁。具体含义不多说了,和ReentrantLock和ReadWriteLock语义相一致。获取锁和释放锁过程均是无阻塞的,返回的token中描述了锁当前的状态和未来可能的状态。线程仅根据返回的future来判断锁的状态,节省了宝贵的时间处理其他业务,特别是我们这种基于Actor模型的服务。

 

上图是锁的状态封装,为什么选择int左移的方式表示状态,下面会有解释。我把锁分为5个状态:

FREE:锁没有被进程中任何线程持有,也没有从Zookeeper上获取任何目录空间。

FETCHING:尝试在多个进程中竞争锁,代表了网络交互的过程。当然过程中可能会发生异常(网络隔离、ZK集群没有处于收敛状态等),若失败,直接进入RELEASING。

RETAINED:已经成功获取了进程锁,这个状态代表当前进程持有该锁。

RELEASING:进程内部已经完成了所有线程的释放过程或者已经开始强制释放进程锁(发生在异常情况下),由最后一根更改其状态为RELEASING,开始进行网络通讯注销其在Zookeeper上的节点。此时新的尝试获得该资源的线程必须等待锁释放完毕后开始新一轮获取。

RELEASED:一个不稳定状态,代表都完事了,马上会变迁到FREE,该状态不允许新线程获取锁。

getFuture():对于锁的下一个状态的描述,由于获取锁的过程是无阻塞的,所以线程可以用自己的方式监控锁未来可能的状态和选择是否无限等待。但线程要知道的是,只有当锁处在RETAINED时,才能进行相应业务处理。而在释放锁的时候,会返给线程一个CompleteFuture,这个Future代表了未来可能的释放进程锁结果:成功或者由于网络、三方服务等原因失败。若失败,线程自己选择需要/不需要回滚对应的操作。

getLockingStatus():返回当前全局锁的状态。

getCurrentSharedCount():告诉当前线程,现在一共有多少根线程(包括自己)持有这把锁。

所以两个可能的状态变迁流程就是:

FREE -> FETCHING -> RETAINED -> RELEASING -> RELEASED:一个正常的流程。

FREE -> FETCHING ------------------> RELEASING -> RELEASED:异常流程。

锁的完整状态由两部分组成:锁处于哪个阶段和当前持有它的线程数,所有改变其内任意一部分状态的操作都应该以原子方式进行。为了能够使用尽量小的代价,尽量高的效率实现原子操作,我选择使用一个无符号int值保存这两者,采用CAS操作进行内部状态变迁。具体是使用高4位保存5种状态,使用底28位保存当前持有该锁的线程数,那么一把独享锁能够支持的最大线程数1<<28 - 1。如下代码片:

 

每把锁使用一个AtomicInteger变量保存内部状态和线程数量。初始化时把锁的状态设置为FREE,持有的线程数为0。使用concurrentMap记录线程在该锁中的重入次数。hahedFolder是实际在ZK上保存的目录名称后缀。monitor用来监控并保存当锁状态处于RELEASING到FREE之前阶段尝试获取锁的线程们,这个阶段所有业务线程处于WAIT状态,在释放ZK虚节点的过程中要等待一段时间,特别是网络情况很差的时候。当然也可以采用被中断的方式结束等待。所有其内部复合状态的更改,只需要做相应位移和位运算即可,计算的效率和刷新到内存的速度我感觉应该是很快的,如下图:

 

下面说说分布式锁的工作流程:以读写进程锁的读锁为例

获取锁:

在初始状态下,获取锁的过程可以大致分为两步,第一步是与ZK集群通讯,尝试创建节点并等待ZK返回节点序列,这一步涉及到网络通讯,线程以持有Future的方式等待。第二步是业务线程更新共享锁的状态并根据锁的当前状态决定自己的行为(比如说锁的状态出错需要回滚自己的已经提交的数据或者若锁的状态是RETAINED可以进行业务处理等等)。由于第一步是进程级别的,所以只有一根线程执行并反馈给业务线程结果即可。这一步工作由专门的通讯线程进行(所有与ZK交互的工作我都选择一组单独的线程池完成,业务线程只等待结果),并把结果更新到当前锁中,也就是说,锁的内部状态直接变迁为FETCHING。

如下图所示,我采用的是CAS方式(关于该方式在并发线程数量不同情况下的表现,在压力测试完成后会进行讨论)。RetrieveSharedReadLockProcessor任务(起的名字不太搭)委派给exec线程池,然后返回当前锁的共享状态SharedReadLock。后续操作操作线程只需要监控它便可,此时业务线程从锁获取中解脱出来,不会浪费额外的时间等待FETCHING -> RETAINED。失败的线程会再次尝试原子判断,但是最多尝试的次数也不会多于5,这种方式相比来说,代价更小,效率更高,当然是有前提条件的。

 

 

下面看下RetrieveSharedReadLockProcessor中异步线程获取进程锁的具体工作流程:

首先在Zookeeper上应用ZAB协议的调单性、顺序性和容错性特性创建一个虚节点,目录结构中属于进程的节点是唯一的,有序的(按先来后到,并在集群中有多个副本)。若此时ZK集群没有处于收敛状态或者出现了无法处理的异常情况,那么直接completeExceptionally通知所有等待的业务线程获取锁失败,锁的状态会在合适的时机被更新为RELEASING,成功的话进入determineGlobalLockStatus。

 

determineGlobalLockStatus:

ZK集群集群处于可提供服务状态并且所有请求处理正常的情况下,会进入该方法中。首先对有序节点(每一个节点代表一个进程)排序,自己是最小的读节点并且在其之前没有写进程节点的话(调单性的和有序性),就成功获取了进程锁。若没有获取成功,我会开启一根线程监控ZK中节点的变化(为了无谓的网络通讯带来的开销,我仅监听离我最近的那个写锁节点的变化情况),并在发现其被移除后重新进入获取进程锁流程。若获取锁成功,则使用原子的方式更新当前锁状态,当然在这个过程中若发现已经没有任何线程持有该锁的话,进入释放锁流程,否则全局锁状态被更新成OBTAINED。

 

 

 

释放锁:

锁的释放过程也可分为两步,第一步线程更新锁内部状态,完成自己的释放过程。第二步是等待当所有线程都释放锁后,进程释放在ZK中创建的虚节点。第二步实际上业务线程可以不关注,但对于一致性较强的操作,比如说在一个典型的“二阶段提交(two phase commit)”类似的场景,监控进程锁的释放结果是必要的。释放锁的过程也使用CAS方式,我在涉及到CAS的操作中都加了competitions记录尝试失败的次数,以便在压力测试之后根据情况进行代码重构。在其发现自己就是那根持有该锁的最后线程并把它自己的重入次数减为0后,会尝试把锁的状态变更为RELEASING,成功变成RELEASING状态后所有新线程不能再次获取锁直到FREE。

 

最后一根释放并成功改变锁状态的线程会发起一个异步任务(ReleaseSharedReadLockProcessor)来释放ZK上的虚节点。返回线程一个公共的未来的释放结果。

 

ReleaseSharedReadLockProcessor:

释放锁的过程和获取十分类似,需要判断当前ZK集群的状态并捕获网络异常及ZK服务异常,就不贴代码了。着重看一下后续两步状态变迁:

由于此时状态是RELEASING,所以没有任何并发问题,在成功删除ZK上的虚节点之后,线程可以安全的把其状态变更为RELEASED。处于这个状态时新来的业务线程会进入InternalStateWrapper的monitor的等待队列中(上面说过了),所以在状态变更为FREE之后需要唤醒这些等待线程重新获取锁。

 

当锁释放失败:

失败情况比较复杂,一共有6种要分开讨论,如下图:

在获取失败后,马上变迁future的状态为completeExceptionally,待所有业务线程发现失败释放锁之后,ZK线程会把锁状态变迁为RELEASING。

这里仅有一点可能会有问题的地方是当已经把future状态变成completeExceptionally,但当前部分业务线程还没有释放锁的情况下,ZK线程就只能无谓的做CAS。为了节约CPU时间,我使用Thread.yield()让出时间片等待业务线程释放锁,但当不断有新线程进来的话,锁状态变迁就一直不能完成,这一点改进吧。

 

 

现在该锁处于严酷的压力测试阶段,先写到这里吧,欢迎挑错指正狠批。

本文是成都网站建设公司、成都网站设计制作公司、成都APP开发公司、成都响应式网站建设、成都VR全景制作-桔子科技公司为您整理!
成都网站建设,成都网站设计,成都网站制作,成都网页设计,成都网站建设公司 ,成都网站设计公司,成都网站制作公司,成都网页设计公司,网站建设网站制作网站设计网页设计成都响应式网站建设、成都响应式网站制作、成都响应式网站开发、成都全景制作、成都VR全景制作成都手机网站建设,手机网站建设,成都APP开发,APP开发,成都建网站,成都做网站,成都商城网站建设,集团网站建设,网站建设,高端网站建设,品牌网站建设,成都平台网站建设,成都响应式网站建设,成都微信网站建设,成都微商城网站建设,成都微信营销,成都微信小程序开发、成都网站优化,成都网络公司。

下一篇:成都网站建设:如何写好网站描述?上一篇:成都网站建设:服务器集群怎么搭建不同的服务和架构

最新案例
手机/微网站
  1. [成都]微信网站建设:微信分销系统能为商铺带来哪些特色服务
  2. [成都]微信网站建设:如何通过微信公众号来推广产品
  3. [成都]微信网站建设:微信开发都有些什么功能
  4. [成都]手机网站:手机网站设计需要达到什么效果
  5. [成都]手机网站:手机网站响应式网站解决方案
  6. [成都]手机网站:手机网站响应式网站解决方案
网络营销
  1. APP开发: APP网页评分功能设计
  2. APP开发:手机APP开发前这4点必须要了解
  3. APP开发:为什么企业要做手机APP
  4. APP开发:你可以更好的留住APP用户
  5. APP开发:开发时间的长短主要由哪些因素决定
  6. APP开发:电子商务类APP开发的4点建议
img

7x24小时售后服务

img

5倍故障时长赔付

img

15天无理由退款

img

N对一管家服务

让我们的顾问联系您

  • 电话:4006-028-024 028-86633922

    邮箱:Service@orangeapp.cn

    成都市成华区崔家店路789号上城国际1-24-9号

qq sina