Zookepper===>动物管理员系统

概述

顾名思义,表示动物管理员。我们将多个服务器看成不同的动物,而Zookeeper就是一个管理员,用来观察这些动物的状态,而我们客户端每次在进行观赏动物之前,都要跟管理员交互,一旦管理员发现有死亡动物,便会通知想观赏该动物的客户。

当然这只是一个普通的比喻,实际上,ZK是由一个领导者和多个从者组成的集群。

1)在该集群中,如果存在半数或超过半数以上的节点死亡,该集群便不能工作。

2)集群全局数据是一致,即每个节点保存的数据都是一致的,但这不影响服务器的压力。

3)数据更新支持原子性,数据读取支持实时性,虽然有多个节点,但数据同步的事件很短。

应用场景

  • 统一命名服务:在一个项目的运行过程中,肯定有多台服务器进行部署,但客户进行访问的时候不可能记得你服务器的ip地址,这个时候就需要域名的作用,而ZK便可以帮助客户通过域名去找寻服务器。
  • 统一配置管理:在分布式开发中,文件数据的同步非常常见,ZK充当观察者,将文件配置统一到一个节点中,其他客户端服务器只监听这个节点就可以了,一旦配置发生改变,其他客户端也能及时知道。
  • 统一集群管理:和统一配置管理思想基本一样,一旦某个节点发生变化,其他节点能及时接受变化信息。
  • 服务器动态上下线:如果一个服务器出现了问题,ZK会立马观察到,这样客户端的访问便可以转为其他服务器或者不访问。
  • 软负载均衡:ZK会观察服务器的负载量合适地分配访问量。

安装

jdk安装

http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 官网下载  jdk-8u171-linux-x64.tar.gz  中间8u171可以不一致,但是要下载.tar.gz结尾的,表示压缩包。

将文件传到 /usr/local目录下。可以在该目录新建一个java文件夹,然后再java文件里再新建两个java和jdk文件夹,最后将压缩包转移到和jdk平级的java目录下。

连接xsheel工具,打开客户端,输入rpm -qa | grep java 命令查看是否安装过 jdk,若什么都没有显示则表示安装过。

若有显示,可能是系统自带的,可通过rpm -e xxx --nodeps  xxx 表示jdk的文件名

通过cd ~ 返回根目录

然后通过cd /usr/local 进入local目录。然后cd java 进入java目录 ,cd jdk 进入带有jdk压缩包的目录。

通过 tar -xvf  jdk-8u171-linux-x64.tar.gz解压文件

解压后,rm -rf jdk-8u171-linux-x64.tar.gz 删除该压缩包,最后ll看只剩下解压后的jdk

cd jdk1.8.0_171/ 进入jdk目录 pwd 显示该目录的全包名,复制,待会配置环境要用

yum -y install vim 命令来安装vim文本编辑器

vim /etc/profile 命令来编辑环境变量的配置文件,进入文件中,键盘向下的键可一直到达文件末端,然后点击键盘字母i进入输入模式,在文件最后加上以下变量

JAVA_HOME=/usr/local/java/jdk1.8.0_171 # 刚才复制的路径
CLASSPATH=.:$JAVA_HOME/lib.tools.jar
PATH=$JAVA_HOME/bin:$PATH
export JAVA_HOME CLASSPATH PATH


修改完成后,点击esc退出编辑模式,然后英文模式下输入:wq 可退出该文件。

退出文件编辑后,source /etc/profile 命令使更改的配置立即生效

最后 java -version 命令和 javac -version 命令来查看 jdk 是否安装成功

ZK 的学习环境一般要给三个客户端,所以开通三个客户端都要安装上jdk环境。

ZK安装

官网下载apache-zookeeper-3.5.7-bin.tar.gz,然后最好在linux的opt目录下新建一个software目录,然后将该压缩包转移到该目录下。

然后登录xsheel客户端,进入该目录,输入 tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/model 该为解压命令,后者为解压到某个目录。

解压完毕后 cd /opt 进入opt目录,cd model 进入model目录,ls查看是否有加压后的bin包。

输入mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7-bin 修改文件名

cd zookeeper-3.5.7-bin/ 进入该目录,cd conf进入该目录进行配置

mv zoo_sample.cfg zoo.cfg 将该conf目录中该文件修改名字

vim zoo.cfg 进入该文件中

# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.
dataDir=/opt/model/zookeeper-3.5.7-bin/zkData


dataDir 该属性设置zk数据存储的位置,zk官方不建议将数据存放在原始的路径中,因为linux会在一定时间刷新数据,到时候会造成数据丢失,所以要更改为自己的路径,这里我在model/zookeeper-3.5.7-bin目录下和zkbin文件的同级目录中新建了一个目录来保存数据。

修改完毕后,点击esc退出编辑模式,然后:wq退出文件。

退出后cd … 返回上一级,之后bin/zkServer.sh start 启动服务端zk,jps -l 或者jps 查看启动情况。

之后 bin/zkCli.sh 启动服务端 启动后出现[zk: localhost:2181(CONNECTED) 0] ,输入ls可查看当前zk的节点,只有一个zk。quit可退出服务端。

退出后bin/zkService.sh status 可查看zk状态 对应bin/zkService.sh stop可停止zk。

ZooKeeper JMX enabled by default
Using config: /opt/model/zookeeper-3.5.7-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: standalone

zoo.cfg解读

# Be sure to read the maintenance section of the 
# The number of milliseconds of each tick
tickTime=2000


服务器与客户端心跳时间,单位毫秒

# The number of ticks that the initial 
# synchronization phase can take
initLimit=10


初始化通信时间,第一次建立连接的时候,表示十个心跳时间为超时

# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5


除了第一次连接,其他次的连接最大通信时间是五个心跳。

数据存放位置和端口号属性略。

集群分发

在开发中,利用集群分发脚本来复制zk到其他服务器上,不用手动再次配一次。

首先,在第一个zk的zookeeper-3.5.7-bin包中新建一个文件夹名为zkData,用来存储数据。

其次,在根目录~中,cd  /bin进入bin包,然后vim xsync 文件,然后写入一下代码,不同的服务器需要更改一些配置

#2. 遍历集群所有机器
for host in 192.168.188.99 192.168.188.100 192.168.188.101

do

  echo ====================  $host  ====================

  #3. 遍历所有目录,挨个发送

  for file in $@

  do

    #4 判断文件是否存在

    if [ -e $file ]

    then

      #5. 获取父目录

      pdir=$(cd -P $(dirname $file); pwd)

      #6. 获取当前文件的名称

      fname=$(basename $file)

      ssh $host "mkdir -p $pdir"

      rsync -av $pdir/$fname $host:$pdir

    else

      echo $file does not exists!

    fi

  done

done

编辑完毕后,要使该文件剩下,在当前目录下bin下,输入chmod 777  xsync分配权限,便可使用。

然后,我们来到 model目录,输入xsync  zookeeper-3.5.7-bin/ 该命令将会将服务器的配置全部复制到其他服务器中。zkData包里的myid文件也是一样的,所以我们需要分配进入其他服务器中,去修改myid文件,修改成不同端口,比如,最原始的myid里是2,那么接下来的机器按顺序便可以为3,4

接下来,进入conf目录,进入zoo.cfg文件,然后在最低端增加如下配置

server.2=192.168.188.99:2888:3888
server.3=192.168.188.100:2888:3888
server.4=192.168.188.101:2888:3888


server.a=b.c.d

a便是刚才配置的myid里的值,该文件是作为zk启动文件。b是该zk所在服务器的端口号,c是这个服务器的follower和leader交换的端口号。d是当其中的服务器出现失灵的情况。需要一个端口号重新进行选举,选举新的端口号。

接下来,便是在第一台服务器中,zookeeper-3.5.7-bin包中输入bin/zkServer.sh start命令,启动zk,然后输入bin/zkServer.sh status就会发现error字样,这是因为其他服务器还未开启,我们按照这个方法启动剩下的服务器,系统就会通过一个选举机制选出谁是follwer和leader

最后,是命令

zk.sh start
zk.sh status
zk.sh stop

地址映射

配置地址映射,能有效加快与zk的连接。

在windows中,找到C:\Windows\System32\drivers\etc 下的hosts文件,如下

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host
# localhost name resolution is handled within DNS itself.
#	127.0.0.1       localhost
#	::1             localhost
#127.0.0.1       activate.navicat.com
192.168.188.99 hadoop1
192.168.188.100 hadoop2
192.168.188.101 hadoop3

如上配置地址映射。

配置完地址映射,更换zoo.cfg配置

server.2=hadoop1:2888:3888
server.3=hadoop2:2888:3888
server.4=hadoop3:2888:3888

这里只需要更换一台服务器便可,然后集群分发脚本,分发出去。但是分发完毕后,记得修改zkDtat里的myid的序列号。

到这里还不完,因为我们是在外面修改的,所以这里只对windows和linux操作系统有效,而对linux系统之间不有效,我们需要在linux系统之间互相配置

比如这里我登录Hadoop1,然后vim /etc/hosts进入hosts文件,在文件的最后面,加入和windows一样的地址映射信息。这样退出后,我们需要用集群分发机制,将这个配置分发去其他服务器。

最后,需要注意的是,既然我们修改的Linux的地址映射,应该就有关输入的ip地址的文件配置都变成映射名,这样才可以生效。

选举机制

  • zk在第一次选举的过程中,会给自己先投一票,接下来第二台,第三台…但是每增加一台服务器给自己投票后,都会跟前一个服务器的id去比较,服务器id小的,会将票数都投给大的,一旦发现票数超过服务器数量一半,则选出leader.
  • zk当不是第一次初始化的时候,选举策略有所改变,这里设计到三个值:SID 和myid一样,不能重复,用来唯一标识zk的服务器id。ZXID,事务ID,用来标识一次服务器状态的变更,在某一时刻,集群中的每台机器不一定完全一致,这和服务器对于客户端的更新要求的处理逻辑有关。EPOCH 每个leader任期的代号,没有leader时,同一轮的逻辑时钟值是一致的,每进行一轮投票后,该值都会增加。当不是第一次初始化的时候,选举策略便是比较这三个值,先比较EPOCH,大的胜出,若相同,比较事务id,大的胜出,若相同,比较myid,大的胜出。

启动停止脚本

  • 跟xsync脚本一样,下面是启动停止脚本的代码,
#!/bin/bash
case $1 in
"start"){
	for i in 服务器ip hadoop103 hadoop104
	do
        echo ---------- zookeeper $i 启动 ------------
		ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
	done
};;
"stop"){
	for i in hadoop102 hadoop103 hadoop104
	do
        echo ---------- zookeeper $i 停止 ------------    
		ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop"
	done
};;
"status"){
	for i in hadoop102 hadoop103 hadoop104
	do
        echo ---------- zookeeper $i 状态 ------------    
		ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"
	done
};;
esac

退出编辑之后,chmod 777 脚本名字 将脚本变得可用 我这里叫的是zk.sh

之后,配置java统一路径,进入zookeeper的bin目录,然后进入zkEnv.sh文件。

if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]];  then
    JAVA="$JAVA_HOME/bin/java"
elif type -p java; then
    JAVA=java


在该代码的上面,为JAVA_HOME变量配置上你的jdk安装路径。

JAVA_HOME="/usr/local/java/jdk/jdk1.8.0_301"

完成后,关闭各个服务器的防火墙。

然后输入zk.sh start 便可以一键启动三台服务器,zk.sh status 查看三台服务器的状态,如果确实三台都启动了,而且都出现error这个报错,首先检查各个服务器的myid文件的端口是否和zoo.cfg这个文件设置的server.id一致。其次是检查zoo.cfg中配置server.id=A:B:C这个A的地址是否对应。最后是检查防火墙是否关闭,注意,防火墙关闭后要重启才可以生效。

启动三台服务器后,便可以启动客户端了,bin/zkCli.sh -server 192.168.188.99:2181

ssh免密登录

配置好启动停止脚本后,发现每次访问其他服务器都要输入密码,很影响效率,所以可以使用ssh免密登录。

这里我们对hadoop1进行配置,需要访问自己,和Hadoop2,Hadoop3。

首先,来到自己的~目录下,输入

[root@dboop1 ~]# ls -al

这个时候会出现所有文件,看是否有.sh文件,如果没有,表示该服务器没有用ssh ip的命令访问过其他服务器,可以尝试访问一次便会出现该文件了。

进入到隐藏文件中cd .ssh/ 便会发现有known_hosts这么一个文件,cat known_hosts 该文件便可以知道该服务器被谁访问过

如果我们在.ssh 文件中发现了其他文件比如:

-rw-------. 1 root root  393 12月 26 03:17 authorized_keys
-rw-------. 1 root root 1679 12月 26 03:16 id_rsa #私钥
-rw-r--r--. 1 root root  393 12月 26 03:16 id_rsa.pub #公钥


就不用输入ssh -keygen -t rsa,若是没有得输入该命令,生成公钥和密钥。

有了公钥和私钥,可以在这台服务器上将公钥和私钥都分发给被授权的服务器

ssh-copy-id hadoop1 ssh-copy-id hadoop2 ssh-copy-id hadoop3 记住,对自己也要分发一次。

然后我们如果站在hadoop1的角度去登录其他服务的话,就不用去输入密码了。

这样,我们站在Hadoop2的角度也做Hadoop1的操作,hadoop3也做统一的操作,这样任何一台服务器都可以互相访问

客户端命令行

启动后,出现下面信息便是启动客户端完毕

[zk: 192.168.188.99:2181(CONNECTED) 1]

help命令可查看客户端命令

	addauth scheme auth
	close 
	config [-c] [-w] [-s]
	connect host:port
	create [-s] [-e] [-c] [-t ttl] path [data] [acl] #创建一个节点,默认创造永久节点,-e 为创造临时节点,-s为创造的节点时带上序列号。
	delete [-v version] path #删除一个节点
	deleteall path #删除多个节点
	delquota [-n|-b] path
	get [-s] [-w] path #加上-s 能获取节点的值,-w可监听数值的变化
	getAcl [-s] path
	history 
	listquota path
	ls [-s] [-w] [-R] path#查看节点信息 -w可监听节点数量的变化
	ls2 path [watch]
	printwatches on|off
	quit 
	reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
	redo cmdno
	removewatches path [-c|-d|-a] [-l]
	rmr path
	set [-s] [-v version] path data # 修改对应节点的值
	setAcl [-s] [-v version] [-R] path acl
	setquota -n|-b val path
	stat [-w] path
	sync path

ls -s / 命令出现下面信息

cZxid = 0x0 #创建节点的事务id
ctime = Thu #Jan 01 08:00:00 CST 1970 #节点被创建的毫秒数,从1970年开始
mZxid = 0x0 #最后更新的事务id
mtime = Thu #Jan 01 08:00:00 CST 1970 #节点最后修改的毫秒数从1970年开始
pZxid = 0x0 #最后更新的子节点事务id
cversion = -1 #版本号,子节点变化后,子节点的修改次数
dataVersion = 0 #节点的数据变化号
aclVersion = 0 #节点访问控制列表的变化号
ephemeralOwner = 0x0 #如果是临时节点,这个是临时节点的session id,如果不是临时节点这个值为0
dataLength = 0 #节点的数据长度
numChildren = 1 #子节点的数量

节点类型

带序号节点

  • 该节点在创建的时候会带上一个序列号。
  • 该种节点分为两类,一类是持久化节点,当客户端和服务端断开连接,节点不会被删除;一类是短暂类节点,当客户端和服务端断开连接,节点会被删除。

不带序列号的

  • 该种节点类型与带序列号的相反,但持久化和短暂节点性质仍然一样。

例子

  • create /hyb 创建一个不带序列号的永久节点
  • create /hyb/zyl 在hyb节点下再创建一个永久节点1
  • create -s /hyb 创建一个带序列号的永久节点
  • create -s /hyb “hyb” 创建一个值为hyb,且带序列号的永久节点
  • get -s /hyb 获取hyb节点下的值
  • set /hyb value 修改hyb节点的值
  • ls /hyb 查看节点hyb的值
  • create -e /hyb创建一个短暂节点hyb
  • 启动和关闭客户端时,持久节点仍然有效,短暂节点会被删除

监听器

在zk中,一台客户端可以注册一个监听器,来观察一些节点的变化。

监听数据的变化:get -w /节点

get -w /zyl监听该节点数据的变化,前提是该节点必须存在

一旦该节点的数据发生变化,监听器所在客户端就会报出下面的话 WatchedEvent state:SyncConnected type:NodeDataChanged path:/zyl

监听子节点的变化:

ls -w /节点 监听该节点的子节点数的变化。

一旦该节点下有子节点创建或删除,监听器所在的客户端就会报如监听该数据那样的变化

虽然两者都会报出警告,但是这种警告只会在第一次监听的时候出现。

API开发

连接

下面展示在IDEA开连接zookeeper。

首先新建一个maven工程,加入以下依赖。

<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4jgroupId>
        <artifactId>log4j-coreartifactId>
        <version>2.8.2version>
    dependency>
    <dependency>
        <groupId>org.apache.zookeepergroupId>
        <artifactId>zookeeperartifactId>
        <version>3.5.7version>
    dependency>
    <groupId>junitgroupId>
    <artifactId>junitartifactId>
    <version>4.13.2version>
    <scope>compilescope>
    dependency>
dependencies>

然后新建一个类

package com.hyb.zk;

import org.apache.zookeeper.*;
import org.junit.Before;
import org.junit.Test;


public class ZkClient {

    private String connectString="hadoop1,hadoop2,hadoop3";

    //连接后,zk发送会话的超时时间
    private int sessionTimeOut=200000;
    private ZooKeeper zooKeeper;
    @Before
    public void init() throws Exception {

        zooKeeper = new ZooKeeper(connectString, sessionTimeOut, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {

            }
        });
    }

    @Test
    public void create()throws Exception{
//        创建一个节点,data为该节点的数据,ZooDefs为访问权限,这里设置面向所有人开启,最后的实参为创建什么类型的节点:持久型
//        注意,创建节点的时候,如果用到@Test注解启动,要在初始化连接的方法上将@Test注解更换为@Before注解,不然会报空指针异常
        zooKeeper.create("/hyb3","hyb".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    }
}

监听器

    @Test
    public void getChildren()throws Exception{
//        获取该节点,true表示默认监听,会走init()中的process()方法,
//        如果你需要自定义监听,可以传入一个匿名类
        List<String> children = zooKeeper.getChildren("/", true);
        for (String child : children) {
            System.out.println(child);
        }
//        让控制台延迟持续输出
        Thread.sleep(Long.MAX_VALUE);
    }

然后写init()的process方法

            @Override
            public void process(WatchedEvent watchedEvent) {
                System.out.println("-----------------");
//                监听的机制是只有第一次监听的时候才会给你提示,及children变量只有在第一次监听的时候才会输出,
//                所以,如果你需要监听器持续输入节点的变化,需要再监听一次
                List<String> children = null;
                try {
                    children = zooKeeper.getChildren("/", true);
                    for (String child : children) {
                        System.out.println(child);
                    }
                    Thread.sleep(Long.MAX_VALUE);
                } catch (KeeperException | InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("-----------------");
            }

监听某个节点是否存在

@Test
public void exit() throws InterruptedException, KeeperException {
    //第一个为节点,第二为是否开启监听
    Stat exists = zooKeeper.exists("/hyb", false);
    System.out.println(exists==null?"no exit":"exit");
}

写数据的流程

客户直接请求leader

因为leader有写权限,所以自己先写一份数据,然后通知其他follower开始写数据。

如果整个集群中,包括leader,超过半数的服务器写完了数据,这个信息便可以回馈给客户。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXna3uEQ-1653707450528)(C:\Users\46894\AppData\Local\Temp\WeChat Files\8c3bed4e7cd3e4b8099a1731b2a0e94.png)]

客户直接请求follower

follower没有写的权限,这个时候会先发通知给leader,leader先写一份,然后通知其他follow开始写,写完的服务器超过一半,leader便可以发送可以反馈的命令给与客户端直接交互的follower,这个follower便会与客户反馈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xb4AiIf5-1653707450530)(C:\Users\46894\AppData\Local\Temp\WeChat Files\056e433b42192b1b57def528f619587.png)]

服务动态上下线

某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知

到主节点服务器的上下线。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-06E4M1ZK-1653707450530)(C:\Users\46894\AppData\Local\Temp\WeChat Files\8a284512afde0ca3248fda02f68093e.png)]

下面来模拟该场景,我们模拟服务器真实上线的过程,而客户端则通过zk集群实时监听服务器的状态。

服务器的真实上线过程,便是在zk下创建节点。而客户端便是监听节点。

首先是服务端

package com.hyb.zk;

import org.apache.zookeeper.*;

import java.io.IOException;

public class ZkTest1 {
    private ZooKeeper zooKeeper;
    private String connectString="hadoop1:2181,hadoop2:2181,hadoop3:2181";
    private int sessionId=2000;

    public static void main(String[] args) throws Exception {
        ZkTest1 zkTest = new ZkTest1();
//        获取zk连接
        zkTest.getConnect();
//        注册zk
        zkTest.register("hadoop1");
//        延迟控制台输出(业务逻辑)
        zkTest.sleep();

    }

    private void sleep() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }

    private void register(String hostName) throws Exception {
        zooKeeper.create("/server/"+hostName,hostName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(hostName+"上线");
    }

    private void getConnect() throws IOException {
        zooKeeper = new ZooKeeper(connectString, sessionId, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {

            }
        });
    }
}

其次是客户端。

package com.hyb.zk;

import org.apache.zookeeper.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class ZkTestClient1 {

    private String connectString="hadoop1:2181,hadoop2:2181,hadoop3:2181";
    private int sessionId=20000;
    private ZooKeeper zk;

    public static void main(String[] args) throws Exception {
        ZkTestClient1 zkTestClient1 = new ZkTestClient1();
//        获取连接
        zkTestClient1.getConnect();
//        监听
        zkTestClient1.watch();
//        业务逻辑,让控制台延迟输出
        zkTestClient1.sleep();
    }

    private void sleep() throws Exception {
        Thread.sleep(Long.MAX_VALUE);
    }

    private void watch() throws InterruptedException, KeeperException {
//        监听器
        List<String> server = zk.getChildren("/server", true);
        List<String> list=new ArrayList<>();
        for (String s :
                server) {
//            获取数据
            byte[] data = zk.getData("/server/" + s, false, null);
            list.add(new String(data));
        }
        System.out.println(list);
    }

    private void getConnect() throws IOException {
        zk=new ZooKeeper(connectString, sessionId, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                try {
//                    再次监听,时刻观察节点变化
                    watch();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

}

测试:首先,我们先开启客户端监听,然后再开启服务端,服务端会创造节点,而实时的,便可以在控制台观察到客户端监听器的节点变化。

分布式锁

多个分布式进程同时共享一个资源,但有且仅有一个进程可以独占,当一个进程用完,另一把锁又开始独占。

分布式锁的实现:创造多个同等级的短暂的带序列号的节点,规定序列号越小,访问资源的权限最高,最容易独占资源,序列号比它大的节点监听独占资源的节点,一旦独占资源的节点释放,权限和其最相似的节点会判断自身权限是否为最高的,一旦是最高的,便独占资源。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6EUHd0hr-1653707450531)(C:\Users\46894\AppData\Local\Temp\WeChat Files\1c00871a41f32e2bff07cb07fd3049c.png)]

原生例子

package com.hyb.zk;

import org.apache.zookeeper.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class DistributedLock {

    private ZooKeeper zk;
    private int sessionTimeout=20000;
    private String connectString="hadoop1:2181,hadoop2:2181,hadoop3:2181";
//    前一个节点路径
    private String upPath;

//    等待
    private CountDownLatch countDownLatch=new CountDownLatch(1);
    private CountDownLatch upCountDownLatch=new CountDownLatch(1);

    private String node;

    public DistributedLock() throws Exception {
//        获取连接
        zk=new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
//                开始监听
//                countDownLatch 检测状态
                if (watchedEvent.getState()==Event.KeeperState.SyncConnected){
//                    如果已经是连接状态,释放
                    countDownLatch.countDown();
                }
//                upCountDownLatch 检测
//                如果前一个节点在删除,前一个节点的路径和upPath相同
                if (watchedEvent.getType()==Event.EventType.NodeDeleted&&watchedEvent.getPath().equals(upPath)){
                    upCountDownLatch.countDown();
                }
            }
        });
//        等待连接后,才执行下一步操作
        countDownLatch.await();

//        判断根节点是否存在
        if (zk.exists("/locks",false)==null){
//            根节点不存在便创建一个永久节点
            zk.create("/locks","locks".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

//    上锁
    public void setLock()throws Exception{
//        创建节点,临时的,有序列号的
        node = zk.create("/locks/lock", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//        判断该节点是否是根节点下序列号最小的节点,权限最高
        List<String> children = zk.getChildren("/locks", false);
//        如果该集合是1,那么便是这个节点可以获取锁
        int size=children.size();
        if (size==1){
            System.out.println(children.get(0)+"获取了锁");
            return;
        }else {
//            将该集合排序
            Collections.sort(children);
//            获取节点在已经排序的集合中的位置如何
//                1.拿到该节点的名字
            String thisNode=node.substring("/locks/".length());
//                2.获取thisNode节点的index
            int index = children.indexOf(thisNode);
//            如果该索引在第一位,那么就不用监听,它自己便可以获取锁了
            if (index==-1){
                System.out.println("出错了");
                return;
            }else if (index==0){
                System.out.println(children.get(0)+"获取锁了");
                return;
            }else {
//                前一个的路径
                upPath="/locks/"+children.get(index-1);
//                监听数据的变化
                zk.getData(upPath,true,null);
//                等待监听完毕
                upCountDownLatch.await();
                return;
            }


        }
    }

//    释放锁
    public void freeLock() throws Exception {
//        删除节点
        zk.delete(node,-1);
    }
}
package com.hyb.zk;

public class DistributedLockTest {

    public static void main(String[] args) throws Exception {

        final DistributedLock distributedLock1 = new DistributedLock();
        final DistributedLock distributedLock2 = new DistributedLock();
//            创建两个互不影响的线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    distributedLock1.setLock();
                    System.out.println("线程一启动");
//                    模拟线程占用
                    Thread.sleep(5*1000);
                    distributedLock1.freeLock();
                    System.out.println("线程一释放");
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    distributedLock2.setLock();
                    System.out.println("线程二启动");
                    Thread.sleep(5*1000);
                    distributedLock2.freeLock();
                    System.out.println("线程二释放");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

框架例子

<dependency>
 <groupId>org.apache.curatorgroupId>
 <artifactId>curator-frameworkartifactId>
 <version>4.3.0version>
dependency>
<dependency>
 <groupId>org.apache.curatorgroupId>
 <artifactId>curator-recipesartifactId>
 <version>4.3.0version>
dependency>
<dependency>
 <groupId>org.apache.curatorgroupId>
 <artifactId>curator-clientartifactId>
 <version>4.3.0version>
dependency>
package com.hyb.zk;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class CuratorLockTest {

    public static void main(String[] args) {
        //        创建分布式锁1
        final InterProcessMutex interProcessMutex1 = new InterProcessMutex(getCuratorWork(), "/locks");
        final InterProcessMutex interProcessMutex2 = new InterProcessMutex(getCuratorWork(), "/locks");
//        创建两个不相干的线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    interProcessMutex1.acquire();
                    System.out.println("1");
                    interProcessMutex1.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    interProcessMutex2.acquire();
                    System.out.println("2");
                    interProcessMutex2.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

    private static CuratorFramework getCuratorWork() {
//        连接失败后重置
        ExponentialBackoffRetry ebr = new ExponentialBackoffRetry(3000, 3);
//        connectionTimeoutMs 连接超时时间 sessionTimeoutMs连接后,发送会话的超时时间
        CuratorFramework build = CuratorFrameworkFactory.builder().connectString("hadoop1:2181,hadoop2:2181,hadoop3:2181")
                .connectionTimeoutMs(20000)
                .sessionTimeoutMs(20000)
                .retryPolicy(ebr).build();
//        客户端启动
        build.start();
        Systemt.println("zk启动成功!");
        return build;
    }

企业面试题

问:zk的选举机制是什么?

首先,是第一次选举:每次选举的过程中都会先给自己投一票,然后跟前面的服务器比较,id小的服务器会将自身的票数给id大的,一旦服务器的票数有超过服务器数量半数的,则选出leader

zk当不是第一次初始化的时候,选举策略有所改变,这里设计到三个值:SID 和myid一样,不能重复,用来唯一标识zk的服务器id。ZXID,事务ID,用来标识一次服务器状态的变更,在某一时刻,集群中的每台机器不一定完全一致,这和服务器对于客户端的更新要求的处理逻辑有关。EPOCH 每个leader任期的代号,没有leader时,同一轮的逻辑时钟值是一致的,每进行一轮投票后,该值都会增加。当不是第一次初始化的时候,选举策略便是比较这三个值,先比较EPOCH,大的胜出,若相同,比较事务id,大的胜出,若相同,比较myid,大的胜出

生产集群安装多少合适:不能过多,过多虽然有提高可靠性的好处,但过多了会让通讯延迟,这是不好的.

经验来说有下面这些比列:

10:3 20:5 100:11 200 :11

你可能感兴趣的:(Spring,Zookeeper)