关于zookeeper:zookeeper的开发应用

4次阅读

共计 12504 个字符,预计需要花费 32 分钟才能阅读完成。

1. zookeeper

1.1. 根底

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,它蕴含一个简略的原语集,分布式应用程序能够基于它实现同步服务,配置保护和命名服务等。很多中间件,比方 kafka、hadoop、hbase,都用到了 Zookeeper 来构建集群。Zookeeper 是 hadoop 的一个子项目,其倒退历程无需赘述。在分布式应用中,因为工程师不能很好地应用锁机制,以及基于音讯的协调机制不适宜在某些利用中应用,因而须要有一种牢靠的、可扩大的、分布式的、可配置的协调机制来对立零碎的状态。Zookeeper 的目标就在于此。

数据结构

了解 ZooKeeper 的一种办法是将其看做一个具备高可用性的文件系统。但这个文件系统中没有文件和目录,而是对立应用节点(node)的概念,称为 znode。znode 既能够保留数据(如同文件),也能够保留其余 znode(如同目录)。所有的 znode 形成一个层次化的数据结构,。

  • Persistent Nodes: 永恒无效地节点, 除非 client 显式的删除, 否则始终存在
  • Ephemeral Nodes: 长期节点, 仅在创立该节点 client 放弃连贯期间无效, 一旦连贯失落,zookeeper 会主动删除该节点
  • Sequence Nodes: 程序节点,client 申请创立该节点时,zk 会主动在节点门路开端增加递增序号, 这种类型是实现分布式锁, 分布式 queue 等非凡性能的要害

集群

生产环境的 zookeeper 服务,个别都是以集群的形式搭建环境。在 zookeeper 集群中,不同节点个别有上面几种角色:

  1. Leader:事务申请的惟一调度者和解决者。保障集群事务处理的程序性。集群外部各服务器的调度者。
  2. Follower:解决客户端非事务申请,转发事务申请给 Leader。参加事务申请 Proposal 的投票。参加 Leader 选举投票。
  3. Observer:解决非事务申请,将事务申请交给 Leader 解决。

可见在 zookeeper 集群中,真正决策的只有一个 Leader 节点,所有的事务申请达到其余节点后,都还是会被转发到 Leader 节点来解决。这种模式,保障了 zookeeper 在命令决策端的原子性。

Leader 选举算法采纳了 Paxos 协定,当少数 Server 写胜利,则工作数据写胜利如果有 3 个 Server,则两个写胜利即可;如果有 4 或 5 个 Server,则三个写胜利即可。Server 数目个别为奇数(3、5、7)如果有 3 个 Server,则最多容许 1 个 Server 挂掉;如果有 4 个 Server,则同样最多容许 1 个 Server 挂掉由此,咱们看出 3 台服务器和 4 台服务器的的容灾能力是一样的,所以为了节俭服务器资源,个别咱们采纳奇数个数,作为服务器部署个数。

原子播送

zookeeper 的外围是原子播送,这个机制保障了各个 server 之间的同步。实现这个机制的协定叫做 Zab 协定。Zab 协定有两种模式,它们别离是 恢复模式 播送模式

当服务启动或者在领导者解体后,Zab 就进入了恢复模式,当领导者被选举进去,且大多数 server 的实现了和 leader 的状态同步当前,恢复模式就完结了。状态同步保障了 leader 和 server 具备雷同的零碎状态。一旦 leader 曾经和少数的 follower 进行了状态同步后,他就能够开始播送音讯了,即进入播送状态。

数据一致性与 paxos 算法

在一个分布式数据库系统中,如果各节点的初始状态统一,每个节点都执行雷同的操作序列,那么他们最初能失去一个统一的状态。

Paxos 算法通过投票来对写操作进行全局编号,同一时刻,只有一个写操作被批准,同时并发的写操作要去争取选票,只有取得过半数选票的写操作才会被批准(所以永远只会有一个写操作失去批准),其余的写操作竞争失败只好再发动一轮投票,就这样,在日复一日年复一年的投票中,所有写操作都被严格编号排序。

编号严格递增,当一个节点承受了一个编号为 100 的写操作,之后又承受到编号为 99 的写操作(因为网络提早等很多不可预感起因),它马上能意识到本人数据不统一了,主动进行对外服务并重启同步过程。任何一个节点挂掉都不会影响整个集群的数据一致性(总 2n+ 1 台,除非挂掉大于 n 台)。

老手不要混同,在这里的投票选举是针对多个客户端有并发写操作时,抢夺该惟一写操作权的选举。和后面说的 zookeeper 集群中,投票选举 master 是不同的概念。尽管它们的选举协定,都是遵循 paxos 算法。

1.2. 装置应用

docker 装置

这里间接应用 zookeeper 官网镜像来装置。

docker run -d \
 --name zookeeper \
 --restart=on-failure:3 \
 -p 2181:2181 \
 -v /Volumes/zookeeper/data/:/data/ \
 zookeeper

启动

执行 docker exec -it 编号 /bin/bash,进入容器外部。

而后在 /bin 目录,执行 ./zkCli.sh,运行启动脚本。

zooInspector 客户端

zooInspector 是 zookeeper 图形化的客户端工具,可用来查看外部数据状况。

可下载 ZooInspector.zip,解压后在 build 目录下获取 zookeeper-dev-ZooInspector.jar。通过 java -jar zookeeper-dev-ZooInspector.jar,即可启动 ZooInspector 图形化客户端。

2. CAP 比照

2.1. CAP 实践

CAP 实践通知咱们,一个分布式系统不可能同时满足以下三种:

  • 一致性(C:Consistency)
  • 可用性(A:Available)
  • 分区容错性(P:Partition Tolerance)

这三个根本需要,最多只能同时满足其中的两项,因为 P 是必须的, 因而往往抉择就在 CP 或者 AP 中。

一致性(C:Consistency)

在分布式环境中,一致性是指数据在多个正本之间是否可能保持数据统一的个性。在一致性的需要下,当一个零碎在数据统一的状态下执行更新操作后,应该保证系统的数据依然处于统一的状态。例如一个将数据正本散布在不同分布式节点上的零碎来说,如果对第一个节点的数据进行了更新操作并且更新胜利后,其余节点上的数据也应该失去更新,并且所有用户都能够读取到其最新的值,那么这样的零碎就被认为具备强一致性(或严格的一致性,最终一致性)。

可用性(A:Available)

可用性是指零碎提供的服务必须始终处于可用的状态,对于用户的每一个操作申请总是可能在无限的工夫内返回后果。“无效的工夫内”是指,对于用户的一个操作申请,零碎必须可能在指定的工夫(即响应工夫)内返回对应的处理结果,如果超过了这个工夫范畴,那么零碎就被认为是不可用的。

分区容错性(P:Partition Tolerance)

分区容错性束缚了一个分布式系统须要具备如下个性:分布式系统在遇到任何网络分区故障的时候,依然须要可能保障对外提供满足一致性和可用性的服务,除非是整个网络环境都产生了故障。

网络分区是指在分布式系统中,不同的节点散布在不同的子网络(机房或异地网络等)中,因为一些非凡的起因导致这些子网络之间呈现网络不连通的情况,但各个子网络的外部网络是失常的,从而导致整个零碎的网络环境被切分成了若干个孤立的区域。须要留神的是,组成一个分布式系统的每个节点的退出与退出都能够看作是一个非凡的网络分区。

2.2. zookeeper 和 eureka 比照

eureka 保障 ap

eureka 优先保障可用性。在 Eureka 平台中,如果某台服务器宕机,Eureka 不会有相似于 ZooKeeper 的选举 leader 的过程;客户端申请会主动切换 到新的 Eureka 节点;当宕机的服务器从新复原后,Eureka 会再次将其纳入到服务器集群治理之中;而对于它来说,所有要做的无非是同步一些新的服务 注册信息而已。

Eureka 各个节点都是平等的,几个节点挂掉不会影响失常节点的工作,残余的节点仍然能够提供注册和查问服务。而 Eureka 的客户端在向某个 Eureka 注册或时如果发现连贯失败,则会主动切换至其它节点,只有有一台 Eureka 还在,就能保障注册服务可用(保障可用性),只不过查到的信息可能不是最新的(不保障强一致性)。

zookeeper 保障 cp

作为一个分布式协同服务,zookeeper 是优先保障一致性的。

进行 leader 选举时集群都是不可用。在应用 ZooKeeper 获取服务列表时,当 master 节点因为网络故障与其余节点失去分割时,残余节点会从新进行 leader 选举。问题在于,选举 leader 的工夫太长,30 ~ 120s, 且选举期间整个 zk 集群都是不可用的,这就导致在选举期间注册服务瘫痪,尽管服务可能最终复原,然而漫长的选举工夫导致的注册长期不可用是不能容忍的。所以说,ZooKeeper 不能保障服务可用性。

3. 利用场景

3.1. 数据公布与订阅(配置核心)

公布与订阅模型,即所谓的配置核心,顾名思义就是发布者将数据公布到 ZK 节点上,供订阅者动静获取数据,实现配置信息的集中式治理和动静更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合应用。

  • 利用中用到的一些配置信息放到 ZK 上进行集中管理。这类场景通常是这样:利用在启动的时候会被动来获取一次配置,同时,在节点上注册一个 Watcher,这样一来,当前每次配置有更新的时候,都会实时告诉到订阅的客户端,素来达到获取最新配置信息的目标。
  • 分布式搜寻服务中,索引的元信息和服务器集群机器的节点状态寄存在 ZK 的一些指定节点,供各个客户端订阅应用。
  • 分布式日志收集零碎。这个零碎的外围工作是收集散布在不同机器的日志。收集器通常是依照利用来调配收集工作单元,因而须要在 ZK 上创立一个以利用名作为 path 的节点 P,并将这个利用的所有机器 ip,以子节点的模式注册到节点 P 上,这样一来就可能实现机器变动的时候,可能实时告诉到收集器调整任务分配。
  • 零碎中有些信息须要动静获取,并且还会存在人工手动去批改这个信息的提问。通常是暴露出接口,例如 JMX 接口,来获取一些运行时的信息。引入 ZK 之后,就不必本人实现一套计划了,只有将这些信息寄存到指定的 ZK 节点上即可。

留神:在下面提到的利用场景中,有个默认前提是:数据量很小,然而数据更新可能会比拟快的场景。

3.2. 负载平衡

这里说的负载平衡是指软负载平衡。在分布式环境中,为了保障高可用性,通常同一个利用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中抉择一个来执行相干的业务逻辑,其中比拟典型的是消息中间件中的生产者,消费者负载平衡。
消息中间件中发布者和订阅者的负载平衡,linkedin 开源的 KafkaMQ 和阿里开源的 metaq 都是通过 zookeeper 来做到生产者、消费者的负载平衡。这里以 metaq 为例如讲下:

生产者负载平衡

metaq 发送音讯的时候,生产者在发送音讯的时候必须抉择一台 broker 上的一个分区来发送音讯,因而 metaq 在运行过程中,会把所有 broker 和对应的分区信息全副注册到 ZK 指定节点上,默认的策略是一个顺次轮询的过程,生产者在通过 ZK 获取分区列表之后,会依照 brokerId 和 partition 的顺序排列组织成一个有序的分区列表,发送的时候依照从头到尾周而复始的形式抉择一个分区来发送音讯。

生产负载平衡

在生产过程中,一个消费者会生产一个或多个分区中的音讯,然而一个分区只会由一个消费者来生产。MetaQ 的生产策略是:

  • 每个分区针对同一个 group 只挂载一个消费者。
  • 如果同一个 group 的消费者数目大于分区数目,则多进去的消费者将不参加生产。
  • 如果同一个 group 的消费者数目小于分区数目,则有局部消费者须要额定承当生产工作。

在某个消费者故障或者重启等状况下,其余消费者会感知到这一变动(通过 zookeeper watch 消费者列表),而后从新进行负载平衡,保障所有的分区都有消费者进行生产。

3.3. 命名服务

zookeeper 的命名服务有两个利用方向,一个是提供相似 JNDI 的性能,利用 zookeepeer 的树型分层构造,能够把零碎中各种服务的名称、地址以及目录信息寄存在 zookeeper,须要的时候去 zookeeper 中读取。

另一个,是利用 zookeeper 程序节点的个性,制作分布式的 ID 生成器,写过数据库利用的敌人都晓得,咱们在往数据库表中插入记录时,通常须要为该记录创立惟一的 ID,在单机环境中咱们能够利用数据库的主键自增性能。但在分布式环境则无奈应用,有一种形式能够应用 UUID,然而它的缺点是没有法则,很难了解。利用 zookeeper 程序节点的个性,咱们能够生成有程序的,容易了解的,同时反对分布式环境的序列号。

3.4. 分布式告诉 / 协调

ZooKeeper 中特有 watcher 注册与异步告诉机制,可能很好的实现分布式环境下不同零碎之间的告诉与协调,实现对数据变更的实时处理。应用办法通常是不同零碎都对 ZK 上同一个 znode 进行注册,监听 znode 的变动(包含 znode 自身内容及子节点的),其中一个零碎 update 了 znode,那么另一个零碎可能收到告诉,并作出相应解决

  • 另一种心跳检测机制:检测零碎和被检测零碎之间并不间接关联起来,而是通过 zk 上某个节点关联,大大减少零碎耦合。
  • 另一种系统调度模式:某零碎有控制台和推送零碎两局部组成,控制台的职责是管制推送零碎进行相应的推送工作。管理人员在控制台作的一些操作,实际上是批改了 ZK 上某些节点的状态,而 ZK 就把这些变动告诉给他们注册 Watcher 的客户端,即推送零碎,于是,作出相应的推送工作。
  • 另一种工作汇报模式:一些相似于工作散发零碎,子工作启动后,到 zk 来注册一个长期节点,并且定时将本人的进度进行汇报(将进度写回这个长期节点),这样工作管理者就可能实时晓得工作进度。

总之,应用 zookeeper 来进行分布式告诉和协调可能大大降低零碎之间的耦合。

3.5. 集群治理与 Master 选举

集群机器监控

这通常用于那种对集群中机器状态,机器在线率有较高要求的场景,可能疾速对集群中机器变动作出响应。这样的场景中,往往有一个监控零碎,实时检测集群机器是否存活。过来的做法通常是:监控零碎通过某种伎俩(比方 ping)定时检测每个机器,或者每个机器本人定时向监控零碎汇报“我还活着”。这种做法可行,然而存在两个比拟显著的问题:

  1. 集群中机器有变动的时候,株连批改的货色比拟多。
  2. 有肯定的延时。

利用 ZooKeeper 有两个个性,就能够实时另一种集群机器存活性监控零碎:

  1. 客户端在节点 x 上注册一个 Watcher,那么如果 x? 的子节点变动了,会告诉该客户端。
  2. 创立 EPHEMERAL 类型的节点,一旦客户端和服务器的会话完结或过期,那么该节点就会隐没。

例如,监控零碎在 /clusterServers 节点上注册一个 Watcher,当前每动静加机器,那么就往 /clusterServers 下创立一个 EPHEMERAL 类型的节点:/clusterServers/{hostname}. 这样,监控零碎就可能实时晓得机器的增减状况,至于后续解决就是监控零碎的业务了。

Master 选举

Master 选举则是 zookeeper 中最为经典的利用场景了。
在分布式环境中,雷同的业务利用散布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络 I / O 解决),往往只须要让整个集群中的某一台机器进行执行,其余机器能够共享这个后果,这样能够大大减少重复劳动,进步性能,于是这个 master 选举便是这种场景下的碰到的次要问题。

利用 ZooKeeper 的强一致性,可能保障在分布式高并发状况下节点创立的全局唯一性,即:同时有多个客户端申请创立 /currentMaster 节点,最终肯定只有一个客户端申请可能创立胜利。利用这个个性,就能很轻易的在分布式环境中进行集群选取了。

另外,这种场景演变一下,就是动静 Master 选举。这就要用到?EPHEMERAL_SEQUENTIAL 类型节点的个性了。

上文中提到,所有客户端创立申请,最终只有一个可能创立胜利。在这里略微变动下,就是容许所有申请都可能创立胜利,然而得有个创立程序,于是所有的申请最终在 ZK 上创立后果的一种可能状况是这样:/currentMaster/{sessionId}-1 ,?/currentMaster/{sessionId}-2 ,?/currentMaster/{sessionId}-3 ….. 每次选取序列号最小的那个机器作为 Master,如果这个机器挂了,因为他创立的节点会马上小时,那么之后最小的那个机器就是 Master 了。

  • 在搜寻零碎中,如果集群中每个机器都生成一份全量索引,不仅耗时,而且不能保障彼此之间索引数据统一。因而让集群中的 Master 来进行全量索引的生成,而后同步到集群中其它机器。另外,Master 选举的容灾措施是,能够随时进行手动指定 master,就是说利用在 zk 在无奈获取 master 信息时,能够通过比方 http 形式,向一个中央获取 master。
  • 在 Hbase 中,也是应用 ZooKeeper 来实现动静 HMaster 的选举。在 Hbase 实现中,会在 ZK 上存储一些 ROOT 表的地址和 HMaster 的地址,HRegionServer 也会把本人以长期节点(Ephemeral)的形式注册到 Zookeeper 中,使得 HMaster 能够随时感知到各个 HRegionServer 的存活状态,同时,一旦 HMaster 呈现问题,会从新选举出一个 HMaster 来运行,从而防止了 HMaster 的单点问题

3.6. 分布式锁

放弃独占

所谓放弃独占,就是所有试图来获取这个锁的客户端,最终只有一个能够胜利取得这把锁。通常的做法是把 zk 上的一个 znode 看作是一把锁,通过 create znode 的形式来实现。所有客户端都去创立 /distribute_lock 节点,最终胜利创立的那个客户端也即领有了这把锁。

管制时序

首先,Zookeeper 的每一个节点,都是一个人造的程序发号器。在每一个节点上面创立子节点时,只有抉择的创立类型是有序(EPHEMERAL_SEQUENTIAL 长期有序或者 PERSISTENT_SEQUENTIAL 永恒有序)类型,那么,新的子节点前面,会加上一个秩序编号。这个秩序编号,是上一个生成的秩序编号加一

比方,创立一个用于发号的节点“/test/lock”,而后以他为父亲节点,能够在这个父节点上面创立雷同前缀的子节点,假设雷同的前缀为“/test/lock/seq-”,在创立子节点时,同时指明是有序类型。如果是第一个创立的子节点,那么生成的子节点为 /test/lock/seq-0000000000,下一个节点则为 /test/lock/seq-0000000001,顺次类推,等等。

其次,Zookeeper 节点的递增性,能够规定节点编号最小的那个取得锁。

一个 zookeeper 分布式锁,首先须要创立一个父节点,尽量是长久节点(PERSISTENT 类型),而后每个要取得锁的线程都会在这个节点下创立个长期程序节点,因为序号的递增性,能够规定排号最小的那个取得锁。所以,每个线程在尝试占用锁之前,首先判断本人是排号是不是以后最小,如果是,则获取锁。

第三,Zookeeper 的节点监听机制,能够保障占有锁的形式有序而且高效。

每个线程抢占锁之前,先抢号创立本人的 ZNode。同样,开释锁的时候,就须要删除抢号的 Znode。抢号胜利后,如果不是排号最小的节点,就处于期待告诉的状态。等谁的告诉呢?不须要其他人,只须要等前一个 Znode 的告诉就能够了。以后一个 Znode 删除的时候,就是轮到了本人占有锁的时候。第一个告诉第二个、第二个告诉第三个,击鼓传花似的顺次向后。

Zookeeper 这种首尾相接,前面监听后面的形式,能够防止羊群效应。所谓羊群效应就是每个节点挂掉,所有节点都去监听,而后做出反映,这样会给服务器带来微小压力,所以有了长期程序节点,当一个节点挂掉,只有它前面的那一个节点才做出反映。

3.7. 分布式队列

队列方面,简略地讲有两种。

一种是惯例的先进先出队列,另一种是要等到队列成员聚齐之后的才对立按序执行。对于第一种先进先出队列,和分布式锁服务中的管制时序场景基本原理统一,这里不再赘述。

第二种队列其实是在 FIFO 队列的根底上作了一个加强。通常能够在 /queue 这个 znode 下事后建设一个 /queue/num 节点,并且赋值为 n(或者间接给 /queue 赋值 n),示意队列大小,之后每次有队列成员退出后,就判断下是否曾经达到队列大小,决定是否能够开始执行了。这种用法的典型场景是,分布式环境中,一个大工作 Task A,须要在很多子工作实现(或条件就绪)状况下能力进行。这个时候,但凡其中一个子工作实现(就绪),那么就去 /taskList 下建设本人的长期时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /taskList 发现自己上面的子节点满足指定个数,就能够进行下一步按序进行解决了。

4. 示例代码

4.1. curator

Curator 是 Netflix 公司开源的一套 Zookeeper 客户端框架。理解过 Zookeeper 原生 API 都会分明其复杂度。Curator 帮忙咱们在其根底上进行封装、实现一些开发细节,包含接连重连、重复注册 Watcher 和 NodeExistsException 等。目前曾经作为 Apache 的顶级我的项目呈现,是最风行的 Zookeeper 客户端之一。从编码格调上来讲,它提供了基于 Fluent 的编程格调反对。

除此之外,Curator 还提供了 Zookeeper 的各种利用场景:Recipe、共享锁服务、Master 选举机制和分布式计数器等。

当初先让咱们看看 Curator 的几种锁计划:

  • InterProcessMutex:分布式可重入排它锁
  • InterProcessSemaphoreMutex:分布式排它锁
  • InterProcessReadWriteLock:分布式读写锁
  • InterProcessMultiLock:将多个锁作为单个实体治理的容器

接下来看上面一个我的项目的示例,我的项目中别离体现了 选举 分布式锁 的例子。

pom.xml

        <!--curator-framework-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.2.0</version>
        </dependency>
        <!--curator-recipes-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.2.0</version>
        </dependency>

CuratorConfig.java

@ConfigurationProperties(prefix = "custom.zookeeper")
@Data
@Configuration
public class CuratorConfig {
    private String connectString;
    private int baseSleepTimeMs;
    private int maxRetries;
    private int sessionTimeoutMs;
    private int connectionTimeoutMs;
    
    /**
     * 注册 CuratorFramework
     * @return
     */
    @Bean
    public CuratorFramework curatorFramework() {RetryPolicy retryPolicy = new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries);
        return CuratorFrameworkFactory
                .builder()
                .connectString(connectString)
                .sessionTimeoutMs(sessionTimeoutMs)
                .connectionTimeoutMs(connectionTimeoutMs)
                .retryPolicy(retryPolicy)
                .build();}
}

CuratorStart.java

@Component
public class CuratorStart implements ApplicationRunner {
    private final CuratorFramework curatorFramework;

    public CuratorStart(CuratorFramework curatorFramework) {this.curatorFramework = curatorFramework;}

    @Override
    public void run(ApplicationArguments applicationArguments){curatorFramework.start();
    }
}

4.2. 示例(选举)

TimerService.java

@Service
@Slf4j
public class TimerService {
    private static final String ZK_NODE_TASK_TIMER="/task-timer";
    DateTimeFormatter dateTimeFormatter= DateTimeFormatter.ofPattern("HH:mm:ss");

    @Value("${server.port}")
    private String serverPort;

    private final CuratorFramework curatorFramework;


    public TimerService(CuratorFramework curatorFramework){this.curatorFramework=curatorFramework;}

    @Scheduled(cron = "*/5 * * * * *")
    public void task(){LeaderLatch leaderLatch=new LeaderLatch(curatorFramework,ZK_NODE_TASK_TIMER);
        try {leaderLatch.start();
            Thread.sleep(2000);
            if (leaderLatch.hasLeadership()){log.warn("* < 主服务 > 是"+serverPort+",以后工夫为"+ LocalDateTime.now().format(dateTimeFormatter));
            }else {log.warn("副服务是"+serverPort+",以后工夫为"+ LocalDateTime.now().format(dateTimeFormatter));
            }
        }catch (Exception e){e.printStackTrace();
        }finally {
            try {leaderLatch.close();
            }catch (Exception e){e.printStackTrace();
            }

        }
    }
}

这里通过 @Scheduled 实现了定时工作,每 5 秒钟执行一次。这是咱们常常头疼的问题,spring 的定时是基于 jvm 过程内的现场池执行的,如果扩大节点,多个 spring 过程同时执行的话,就会反复执行定时工作。

那么这里咱们别离指定 port 为 8001、8002、8003,别离运行 3 个程序。当它们每隔 5 秒同时触发办法执行时,就会在 zookeeper 中模仿一个选举,最终只有一个程序作为 < 主服务 > 被执行。

4.3. 示例(分布式锁)

EmployeeController.java

@Slf4j
@RestController
public class EmployeeController {DateTimeFormatter dateTimeFormatter= DateTimeFormatter.ofPattern("HH:mm:ss");

    private final CuratorFramework curatorFramework;

    public EmployeeController(CuratorFramework curatorFramework) {this.curatorFramework = curatorFramework;}

    /**
     * InterProcessMutex 通过在 zookeeper 的某门路节点下创立长期序列节点来实现分布式锁,即每个线程(跨过程的线程)获取同一把锁前,都须要在同样的门路下创立一个节点,节点名字由 uuid + 递增序列组成。而通过比照本身的序列数是否在所有子节点的第一位,来判断是否胜利获取到了锁。当获取锁失败时,它会增加 watcher 来监听前一个节点的变动状况,而后进行期待状态。直到 watcher 的事件失效将本人唤醒,或者超时工夫异样返回。*
     * @param key
     * @return
     */
    @GetMapping("/demo/{key}")
    public String save(@PathVariable("key") String key) {
        // 获取锁
        InterProcessSemaphoreMutex balanceLock = new InterProcessSemaphoreMutex(curatorFramework, "/zktest" + key);
        try {
            // 执行加锁操作
            balanceLock.acquire();
            log.warn("lock《, key=" + key+", 以后工夫为"+ LocalDateTime.now().format(dateTimeFormatter));

            Thread.sleep(10000);

        } catch (Exception e) {e.printStackTrace();
        } finally {
            try {
                // 开释锁资源
                balanceLock.release();
                log.warn("unlock》, key=" + key+", 以后工夫为"+ LocalDateTime.now().format(dateTimeFormatter));
            } catch (Exception e) {e.printStackTrace();
            }
        }
        return key;
    }
}

当咱们模仿不同的申请竞争同一个 key 时,每次加锁,线程 10 秒钟后,再解锁,最初其余的申请才能够再加锁,反复之前的操作。在同一个程序内,我能够通过 jdk 的线程锁来实现相似的性能,但如果咱们想要实现在不同的程序中都能够如此加锁,就只能通过分布式锁来实现。

正文完
 0