distributed-system
Last updated
Last updated
降级 当我们的服务器压力剧增为了保证核心功能的可用性 ,而选择性的降低一些功能的可用性,或者直接关闭该功能,以此释放服务器资源以保证核心任务的正常运行。
熔断 在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断
。在固定时间窗口内,接口调用超时/异常比率达到一个阈值,会开启熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级
的效果。 熔断器的工作机制主要是关闭、打开和半打开这三个状态之间的切换。在正常情况下,熔断器是关闭的;当调用方调用下游服务出现异常时,熔断器会收集异常指标信息,当达到熔断条件时熔断器打开,这时调用端再发起请求是会直接被熔断器拦截,并快速地执行失败逻辑;熔断器经过一段时间后,会尝试转为半打开状态,这时熔断器允许调用方发送一个请求给服务端,如果这次请求能够正常地得到服务端的响应,则将状态置为关闭状态,否则设置为打开。
限流 防止瞬时流量过大,达到限流阈值后,请求会被直接拒绝。
1.第一阶段:准备/投票阶段
该阶段的主要目的在于打探数据库集群中的各个参与者是否能够正常的执行事务,具体步骤如下:
协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果;
事务参与者收到请求之后,执行事务但不提交,并记录事务日志;
参与者将自己事务执行情况反馈给协调者,同时阻塞等待协调者的后续指令。
2.第二阶段:提交阶段
在经过第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在 3 种可能性:
所有的参与者都回复能够正常执行事务(提交事务)
一个或多个参与者回复事务执行失败(回滚事务)
协调者等待超时(回滚事务)
提交事务:
协调者向各个参与者发送 commit 通知,请求提交事务;
参与者收到事务提交通知之后执行 commit 操作,然后释放占有的资源;
参与者向协调者返回事务 commit 结果信息。
回滚事务
协调者向各个参与者发送事务 rollback 通知,请求回滚事务;
参与者收到事务回滚通知之后执行 rollback 操作,然后释放占有的资源;
参与者向协调者返回事务 rollback 结果信息。
优点: 原理简单,容易实现。
缺点: 1.单点问题。协调者所在服务器宕机影响整个集群正常工作。 2.同步阻塞,性能低下。两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率极其低下。 3.数据不一致性。比如在第二阶段中,假设协调者发出了事务 commit 通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
应用 MySQL InnoDB 事务。
与两阶段提交不同的是,三阶段提交有两个改动点。
引入超时机制。同时在协调者和参与者中都引入超时机制。
在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
1.第一阶段:CanCommit阶段
协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的执行事务,这个过程是轻量的。
2.第二阶段:PreCommit阶段 协调者根据参与者在询问阶段的响应判断是否执行事务还是中断事务
如果所有参与者都返回Yes,则执行事务,并将 Undo 和 Redo 信息记录到事务日志中
如果参与者有一个或多个参与者返回No或者超时,则中断事务
参与者执行完操作之后返回ACK响应,同时开始等待最终指令。
3.第三阶段:DoCommit阶段
调者将会依据事务执行返回的结果来决定提交或回滚事务。
该阶段进行真正的事务提交,分为以下两种情况: ①执行提交
发送提交请求:协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
事务提交:参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
响应反馈:事务提交完之后,向协调者发送Ack响应。
完成事务:协调者接收到所有参与者的ack响应之后,完成事务。
②中断事务:协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务
发送中断请求:协调者向所有参与者发送abort请求
事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息
中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
缺点: 数据不一致。在提交阶段如果发送的是中断事务请求,但是由于网络问题,导致部分参与者没有接到请求,那么参与者会在等待超时之后执行提交事务操作,这样这些由于网络问题导致提交事务的参与者的数据就与接受到中断事务请求的参与者存在数据不一致的问题。(相对于2PC,减少了阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行提交事务,而不会一直持有事务资源并处于阻塞状态)
3PC 和 2PC 区别
3PC将2PC的第一阶段拆分为两个,分为CanCommit和PreCommit,CanCommit比较轻量,事先查询参与者是否具备事务执行条件,能够提高事务效率,尽量避免后续事务回滚,2PC过于激进
参与者超时机制,PreCommit超时后参与者会放弃该事务,DoCommit超时后参与者会自动提交该事务。避免像2PC那样超时一直阻塞等待
核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
分为三个阶段:
Try 阶段主要是对业务系统做检测及资源预留。
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。要求具备幂等设计,Confirm 失败后需要进行重试。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。Cancel 操作满足幂等性。
以电商系统举例
Try 阶段锁定库存、预增加积分、订单预创建
Confirm 阶段扣减库存、增加会员积分、创建订单
Cancel 阶段返还锁定库存、扣减预增加积分、删除预订单
TCC属于应用层的一种补偿方式。需要自己实现Try、Confirm、Cancel业务逻辑。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。
角色解释
Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
一个分布式事务在Seata中的执行流程:
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
XID 在微服务调用链路的上下文中传播。
RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
通常所说的柔性事务分为:两阶段型、补偿型、异步确保型、最大努力通知型几种。
1.客户端注册
客户端注册是服务自身要负责注册与注销的工作。当服务启动后向注册中心注册自身,当服务下线时注销自己。期间还需要和注册中心保持心跳。心跳不一定要客户端来做,也可以由注册中心负责(这个过程叫探活)。
这种方式的缺点是注册工作与服务耦合在一起,不同语言都要实现一套注册逻辑。
2.第三方注册(独立的服务 Registrar)
第三方注册由一个独立的服务Registrar负责注册与注销。当服务启动后以某种方式通知Registrar,然后 Registrar 负责向注册中心发起注册工作。同时注册中心要维护与服务之间的心跳,当服务不可用时,向注册中心注销服务。
这种方式的缺点是 Registrar 必须是一个高可用的系统,否则注册工作没法进展。
3.客户端发现
客户端发现是指客户端负责查询可用服务地址,以及负载均衡的工作。这种方式最方便直接,而且也方便做负载均衡。再者一旦发现某个服务不可用立即换另外一个,非常直接。
缺点也在于多语言时的重复工作,每个语言实现相同的逻辑。
5.服务端发现
服务端发现需要额外的 Router 服务,请求先打到 Router,然后 Router 负责查询服务与负载均衡。
这种方式虽然没有客户端发现的缺点,但是它的缺点是保证 Router 的高可用。
持久节点(PERSISTENT) 所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。
持久顺序节点(PERSISTENT_SEQUENTIAL) 这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。 在创建节点的时候只需要传入节点 “/test_”,这样之后,zookeeper自动会给”test_”后面补充数字。
临时节点(EPHEMERAL) 和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。 这里还要注意一件事,就是当你客户端会话失效后,所产生的节点也不是一下子就消失了,也要过一段时间,大概是10秒以内,可以试一下,本机操作生成节点,在服务器端用命令来查看当前的节点数目,你会发现客户端已经stop,但是产生的节点还在。
临时顺序节点(EPHEMERAL_SEQUENTIAL) 此节点是属于临时节点,不过带有顺序,客户端会话结束节点就消失。
Leader
1.一个 Zookeeper 集群同一时间只会有一个实际工作的 Leader,它会发起并维护与各 Follwer及 Observer 间的心跳。
2.所有的写操作必须要通过 Leader 完成再由 Leader 将写操作广播给其它服务器。只要有超过半数节点(不包括 observeer 节点)写入成功,该写请求就会被提交(类 2PC 协议)。
Follower
1.一个 Zookeeper 集群可能同时存在多个 Follower,它会响应Leader 的心跳。
2.Follower 可直接处理并返回客户端的读请求,同时会将写请求转发给 Leader 处理。
3.并且负责在 Leader 处理写请求时对请求进行投票。
Observer
角色与 Follower 类似,但是无投票权。Zookeeper 需保证高可用和强一致性,为了支持更多的客户端,需要增加更多 Server;Server 增多,投票阶段延迟增大,影响性能;引入 Observer,Observer 不参与投票; Observers 接受客户端的连接,并将写请求转发给 leader 节点; 加入更多 Observer 节点,提高伸缩性,同时不影响吞吐率。
Leader election(选举阶段-选出准 Leader)
Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。只有到达 广播阶段(broadcast) 准 leader 才会成为真正的 leader。这一阶段的目的是就是为了选出一个准 leader,然后进入下一个阶段。
Discovery(发现阶段-接受提议、生成 epoch、接受 epoch)
Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。这个一阶段的主要目的是发现当前大多数节点接收的最新提议,并且准 leader 生成新的 epoch,让 followers 接受,更新它们的 accepted Epoch
一个 follower 只会连接一个 leader,如果有一个节点 f 认为另一个 follower p 是 leader,f在尝试连接 p 时会被拒绝,f 被拒绝之后,就会进入重新选举阶段。
Synchronization(同步阶段-同步 follower 副本)
Synchronization(同步阶段):同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。只有当 大多数节点都同步完成,准 leader 才会成为真正的 leader。follower 只会接收 zxid 比自己的 lastZxid 大的提议。
Broadcast(广播阶段-leader 消息广播)
Broadcast(广播阶段):到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。
PS.ZAB 协议 JAVA 实现(FLE-发现阶段和同步合并为 Recovery Phase(恢复阶段))
协议的 Java 版本实现跟上面的定义有些不同,选举阶段使用的是 Fast Leader Election(FLE),它包含了 选举的发现职责。因为 FLE 会选举拥有最新提议历史的节点作为 leader,这样就省去了发现最新提议的步骤。实际的实现将 发现阶段 和 同步合并为 Recovery Phase(恢复阶段)。所以,ZAB 的实现只有三个阶段:Fast Leader Election;Recovery Phase;Broadcast Phase。
每个 sever 首先给自己投票,然后用自己的选票和其他 sever 选票对比,权重大的胜出,使用权重较大的更新自身选票箱。具体选举过程如下:
每个 Server 启动以后都询问其它的 Server 它要投票给谁。对于其他 server 的询问,server 每次根据自己的状态都回复自己推荐的 leader 的 id 和上一次处理事务的 zxid(系统启动时每个 server 都会推荐自己)
收到所有 Server 回复以后,就计算出 zxid 最大的那个Server,并将这个 Server 相关信息设置成下一次要投票的 Server
计算这过程中获得票数最多的的 sever 为获胜者,如果获胜者的票数超过半数,则该 server 被选为 leader。否则,继续这个过程,直到 leader 被选举出来
leader 就会开始等待 server 连接
Follower 连接 leader,将最大的 zxid 发送给 leader
Leader 根据 follower 的 zxid 确定同步点,至此选举阶段完成
选举阶段完成 Leader 同步后通知 follower 已经成为 uptodate 状态
Follower 收到 uptodate 消息后,又可以重新接受 client 的请求进行服务了
Follower 收到其他节点的投票信息后,会跟自身进行相应的比较:首先比较 zxid 的大小,如果相等,再比较 server id 的大小。如果收到的投票的 zxid 或 server id 比自己的大,那么更新本地已收到的投票信息。
目前有 5 台服务器,每台服务器均没有数据,它们的编号分别是 1,2,3,4,5,按编号依次启动,它们的选择举过程如下:
服务器 1 启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器 1 的状态一直属于 Looking。
服务器 2 启动,给自己投票,同时与之前启动的服务器 1 交换结果,由于服务器 2 的编号大所以服务器 2 胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是 LOOKING。
服务器 3 启动,给自己投票,同时与之前启动的服务器 1,2 交换信息,由于服务器 3 的编号最大所以服务器 3 胜出,此时投票数正好大于半数,所以服务器 3 成为领导者,服务器1,2 成为小弟。
服务器 4 启动,给自己投票,同时与之前启动的服务器 1,2,3 交换信息,尽管服务器 4 的编号大,但之前服务器 3 已经胜出,所以服务器 4 只能成为小弟。
服务器 5 启动,后面的逻辑同服务器 4 成为小弟。
监听事件
nodedatachanged # 节点数据改变
nodecreate # 节点创建事件
nodedelete #节点删除事件
nodechildrenchanged # 子节点改变事件
基于Paxos算法的ZAB协议(原子广播,崩溃恢复)
两阶段提交+过半写机制
ZooKeeper写数据的机制是客户端把写请求发送到leader节点上(如果发送的是follower节点,follower节点会把写请求转发到leader节点),leader节点会把数据通过proposal请求发送到所有节点(包括自己),所有到节点接受到数据以后都会写到自己到本地磁盘上面,写好了以后会发送一个ack请求给leader,leader只要接受到过半的节点发送ack响应回来,就会发送commit消息给各个节点,各个节点就会把消息放入到内存中(放内存是为了保证高性能),该消息就会用户可见了。
崩溃恢复机制
当leader宕机以后,ZooKeeper会选举出来新的leader,新的leader启动以后要到磁盘上面去检查是否存在没有commit的消息,如果存在,就继续检查看其他follower有没有对这条消息进行了commit,如果有过半节点对这条消息进行了ack,但是没有commit,那么新对leader要完成commit的操作。
CAP 原则又称 CAP 定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
一致性(C)
在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
可用性(A)
在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
分区容忍性(P)
以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。
通俗解释: 一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。
当你一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。
然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。
要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。
总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。
AP举例 1.MySQL分库,更新操作主库成功了,就返回成功,放弃了数据一致性。因为如果出现网络延迟,数据没有及时同步到从库,导致数据不一致。但主从mysql照样可以提供服务,也就是保证了可用性A。 2.NoSQL如DynamoDB,通常主表会有三个备份,默认情况下写入到两个副本中即认为写入成功,因此在高频读取时也有一定的概率读到未完成写入的数据。GSI 读写会产生一致性的原因是 GSI 和主表实际是 两个不同的存储,写入到主表的数据会通过流同步到GSI,这个过程会存在一定的延时(10ms级别),因此在读取 GSI 中的数据时,不能设置为强一致性。
CP举例 1.MySQL分库,更新操作主从库都成功了,才返回成功,保证了数据一致性,因为主从mysql更新数据都成功才算成功,但网络出现问题时,主mysql无法访问从节点,导致写操作一直不成功。其实就是放弃了可用性,只满足CP原则,系统只能提供读服务。 2.Zookeeper,不能保证每次请求的可用性(极端情况下ZK会丢弃一些请求),进行leader选举时集群都是不可用的
CA举例 分布式系统肯定要实现P,那其实CA只能是单机系统。 如单机数据库,或者是共享存储数据库,比如 Aurora DB 类似的思路设计的数据库,共享同一份存储,上面建立不同的 MySQL 进程,一个 MySQL 读写,其他的只读,由于使用的同一块存储,并且只有一个 MySQL 进程写入,满足 ACID 的事务特性,能保证强一致性,以及可用性。
UUID
优点
代码实现简单。
本机生成,没有性能问题
因为是全球唯一的ID,所以迁移数据容易
缺点
每次生成的ID是无序的,无法保证趋势递增
UUID的字符串存储,查询效率慢
存储空间大
ID本身无业务含义,不可读
适用场景
类似生成token令牌的场景
不适用一些要求有趋势递增的ID场景
MySQL主键自增
利用了MySQL的主键自增auto_increment,默认每次ID加1。
优点
数字化,id递增
查询效率高
具有一定的业务可读
缺点
存在单点问题,如果mysql挂了,就没法生成iD了
数据库压力大,高并发抗不住
MySQL多实例主键自增
解决mysql的单点问题,重新设置他的 auto_increment_increment和auto_increment_offset值。如N台数据库,每台的初始值auto_increment_offset分别为1,2,3,...,N,auto_increment_increment为N。
优点
解决了单点问题
缺点
一旦把步长定好后,就无法扩容;而且单个数据库的压力大,数据库自身性能无法满足高并发
应用场景
数据不需要扩容的场景
雪花snowflake算法
雪花算法生成64位的二进制正整数,然后转换成10进制的数。64位二进制数由如下部分组成:
1位标识符:始终是0
41位时间戳:41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截 )得到的值,这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的
10位机器标识码:可以部署在1024个节点,如果机器分机房(IDC)部署,这10位可以由 5位机房ID + 5位机器ID 组成
12位序列:毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
优点
此方案每秒能够产生409.6万个ID,性能快
时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增
灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求
缺点
依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成
Redis生成方案 利用redis的incr原子性操作自增,一般算法为: 年份 + 当天距当年第多少天 + 天数 + 小时 + redis自增
优点
有序递增,可读性强
缺点
占用带宽,每次要向redis进行请求(redis和ID生成不在同一机器)
实际生产中的一些优化
ID生成服务可提前生成ID
Client可获取一批ID,不用频繁请求
ID生成服务加分布式锁
基于数据库实现
在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
缺点
因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;
不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。
基于Redis实现
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
获取锁
使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
释放锁
释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
优点
高性能
超时机制
缺点
没有锁等待队列
基于zookeeper实现
Zookeeper分布式锁应用了临时顺序节点。
获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1
Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前(最小)的一个。如果是第一个节点,则成功获得锁
Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2,Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态
Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态
释放锁
任务完成,客户端显示释放。 Client1会显示调用删除节点Lock1的指令。
任务执行过程中,客户端崩溃。 获得锁的Client1在任务执行过程中,如果崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。
优点
高可用
可重入
阻塞锁
有等待锁的队列
缺点
性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。