zookeeper笔记(一)——基础应用

一、zookeeper入门

1.1 概述

  zookeeper是一个开源的分布式的,为分布式框架提供协调服务的Apache项目。

  (1)服务器启动时去注册信息(创建的都是临时节点)

  (2)获取到当前在线服务器列表,并且注册监听

  (3)服务器节点下线

  (4)服务器节点上下线通知

  (5)process()重新再去获取服务器列表,并注册监听

1.2 特点

  (1)Zookeeper:一个领导者(Leader),多个跟随者(Follower)组成的集群。

  (2)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所以Zookeeper适合安装奇数台服务器

  (3)全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。

  (4)更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行。

  (5)数据更新原子性,一次数据更新要么成功,要么失败。

  (6)实时性,在一定时间范围内,Client能读到最新数据。

1.3 数据结构

  ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个ZNode。每一个ZNode默认能够存储1MB的数据,每个 ZNode 都可以通过其路径唯一标识

1.4 应用场景

  提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等。

(1)统一命名服务

  在分布式环境下,经常需要对应用/服务进行统一命名,便于识别

  例如:IP不容易记住,但是域名容易记住

(2)统一配置管理

  1、分布式环境下、配置文件同步非常常见

    a. 一般要求一个集群中,所有节点的配置信息是一致的,比如kafka集群

    b. 对配置文件修改后,希望能够快速同步到各个节点上

  2、配置管理可以交给ZooKeeper实现。

    a. 可以将配置信息写入ZooKeeper上的一个Znode

    b. 各个客户端服务器监听这个Znode。

    c. 一旦Znode中的数据被修改,ZooKeeper将通知各个客户端服务器

(3)统一集群管理

  1、分布式环境中,实时掌握每个节点的状态是必要的

    a. 可以根据节点实时状态做出一些调整

  2、ZooKeeper可以实现实时监控节点状态个变化

    a. 可以将节点信息写入ZooKeeper上的一个ZNode

    b, 监听这个ZNode可获取它的实时状态变化

(4)服务器动态上下线

 (5)软负载均衡

  在ZooKeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求

 

 

二、ZooKeeper本地安装

1、本地模式安装部署

  详细安装步骤见文章Hadoop+微服务 环境搭建笔记

2、配置参数解读

  ZooKeeper中的配置文件zoo.cfg中参数含义解读如下:

1、tickTime=2000:通知心跳数,ZooKeeper服务器与客户端心跳时间,单位毫秒
    ZooKeeper使用的基本时间,服务器之间或者客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
    它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)

2、initLimit=10:LF初始通信时限
    集群中的Follower跟随者服务器与Leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的ZooKeeper服务器连接到Leader的时限。

3、syncLimit=5:LF同步通信时限
    集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * initLimit,Leader认为Follwer死掉,从服务器列表中删除Follwer。

4、dataDir:数据文件目录+数据持久化路径
    主要用于保存ZooKeeper中的数据。

5、clientPort=2182:客户端连接端口
    监听客户端连接的端口

 

 

三、ZooKeeper集群操作

1、集群操作

  1.1、集群安装

  详细安装步骤见文章Hadoop+微服务 环境搭建笔记

  1.2、选举机制(面试重点)

  1.3、ZK集群启动停止脚本

    (1)在hadoop102的/home/aitguigu/bin 目录下创建脚本

cd bin/

pwd

ll

vim zk.sh

      在脚本中编写如下内容

#!/bin/bash
case $1 in
"start"){
for i in hadoop102 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

 

 保存后,发现脚本zk.sh灰色,没有变绿色,也就是说没有执行权限

    (2)增加脚本执行权限

chmod u+x zk.sh

 

   变绿色了,具有执行权限了

       (3)Zookeeper集群启动脚本

 题外话,这里我们为了方便查看三台服务器Java进程(不需要到每个服务器查看jps),写了一个jpsall的脚本,过程如下

cd bin/

pwd

ll

vim jpsall

 保存后退出,然后赋予脚本执行权限

chmod u+x jpsall

   好了,jpsall脚本创建完毕,回到整体,我们来测试一下吧,并启动Zookeeper集群脚本

jpsall

zk.sh start

启动之前

 启动之后

   (3)Zookeeper集群停止脚本

zk.sh stop

 

 2、客户端命令行操作

  2.1、命令行语法

   (1)启动客户端

cd /opt/module/zookeeper-3.5.7/

bin/zkCli.sh -serverhadoop102:2181

 

启动完毕之后,前面显示的前缀变了

   (2)显示所有操作命令

help

  2.2、znode节点数据信息

  (1)查看当前znode中所包含的内容

ls /

 

   (2)查看当前节点详细数据

ls -s /

 

    (a)czxid:创建节点的事务 zxid

  每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生

    (b)ctime:znode 被创建的毫秒数(从 1970 年开始)

    (c)mzxid:znode 最后更新的事务 zxid

    (d)mtime:znode 最后修改的毫秒数(从 1970 年开始)

    (e)pZxid:znode 最后更新的子节点 zxid

    (f)cversion:znode 子节点变化号,znode 子节点修改次数

    (g)dataversion:znode 数据变化号

    (h)aclVersion:znode 访问控制列表的变化号

    (i)ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0。

    (j)dataLength:znode 的数据长度

    (k)numChildren:znode 子节点数量

  2.3、节点类型(持久/短暂/有序号/无序号)

  (1) 分别创建2个普通节点(永久节点+不带序号)

create /sanguo "diaochan"

ls /

create /sanguo/shuguo "liubei"

ls /

ls /sanguo

   注意:创建节点的时候,要赋值

  (2)获取节点的值

get -s /sanguo

get -s /sanguo/shuguo

  (3)创建带序号的节点(永久节点+带序号)

    (a)先创建一个普通的根节点/sanguo/weiguo

create /sanguo/weiguo "caocao"

     (b)创建带序号的节点

create -s /sanguo/weiguo/zhangliang "zhangliang"

create -s /sanguo/weiguo/zhangliang "zhangliang"

create /sanguo/weiguo "caocao"

create -s /sanguo/weiguo/xuchu "xuchu"

create -s /sanguo/weiguo/xuchu "xuchu"

   结论:带序号的节点可以重复的创建,多个序号的节点之间使用序号来区分,不带序号的节点不能重复创建,只能创建一次

      如果原来没有序号节点,序号从0开始依次递增。如果原节点下已经有2个节点,则再排序时从2开始,以此类推。

这里,我们来测试一下,退出客户端之后,节点是否有被删除

#退出客户端
quit

#重新进入客户端
bin/zkCli.sh -serverhadoop102:2181

#查看节点是否存在
ls /sanguo

ls /sanguo/weiguo

 测试结果发现,节点仍然存在,说明创建的确实是永久节点

  (4)创建短暂节点(短暂节点+不带序号 or 带序号)

     (a)创建短暂的不带序号的节点

create -e /sanguo/wuguo "zhouyu"

ls /sanguo

     (b)创建短暂的带序号的节点

create -e -s /sanguo/wuguo "zhouyu"

ls /sanguo

 

 这里,我们来测试一下,退出客户端之后,节点是否有被删除

#退出客户端
quit

#重新进入客户端
bin/zkCli.sh -serverhadoop102:2181

#查看节点是否存在
ls /sanguo

 

 测试结果发现创建的短暂节点wuguo已经被删除了

  (5)修改节点数据值

#获取修改之前的值
get -s /sanguo/weiguo

#修改节点的值
set /sanguo/weiguo "simayi"

#获取修改之后的值
get -s /sanguo/weiguo

 

   2.4、监听器原理

  客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序。

   (1)节点的值变化监听

    (a)在hadoop104主机上注册监听/sanguo节点数据变化

#切换目录
cd /opt/module/zookeeper-3.5.7/

#启动zk客户端
bin/zkCli.sh

#查看节点sanguo的值
get -s /sanguo

#注册一个监听器,监听节点sanguo的变化
get -w /sanguo

    (b)在hadoop103主机上修改/sanguo节点的数据

#切换目录
cd /opt/module/zookeeper-3.5.7/

#启动zk客户端
bin/zkCli.sh

#修改节点sanguo的值
set /sanguo "xishi"

 hadoop103上节点/sanguo修改命令执行完毕后,我们看看hadoop104上的变化

 hadoop104显示节点变化了

我们再来修改一次,看看hadoop104是否还会收到通知

注意:在hadoop103再多次修改/sanguo的值,hadoop104上不会再收到监听。因为注册一次,只能监听一次。想再次监听,需要再次注册

   (2)节点的子节点变化监听(路径变化)

    (a)在hadoop104主机上注册监听/sanguo 节点的子节点变化

ls -w /sanguo

     (b)在hadoop103主机 /sanguo 节点上创建子节点

create /sanguo/jin "simayi"

 

 hadoop103上节点/sanguo修改命令执行完毕后,我们看看hadoop104上的变化

hadoop104显示节点变化了

我们再来修改一次,看看hadoop104是否还会收到通知

发现没有再次收到变化通知,所以:节点的路径变化,也是注册一次,生效一次。想多次生效,就需要多次注册。

  2.5、节点删除与查看

  (1)删除节点

ls /

ls /sanguo

delete /sanguo/jin

delete /sanguo

   上图中,我们发现,当我们想删除整个 /sanguo 节点的时候报错了,说节点非空,那么我们如何删除整个 /sanguo节点以及它的子节点呢

  (2)递归删除节点

deleteall /sanguo

ls /sanguo

  (3)查看节点状态

stat /sanguo

3、客户端API操作

  前提:保证hadoop102、hadoop103、hadoop104服务器上Zookeeper集群服务端启动。

  3.1、IDEAL环境搭建

  (1)创建一个工程:zookeeper

  (2)添加pom.xml文件

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.5.7</version>
        </dependency>
    </dependencies>

   (3)拷贝log4j.properties文件到项目根目录

需要在项目的 src/main/resources 目录下,新建一个文件,命名为“log4j.properties”,在文件中填入。
log4j.rootLogger=INFO, stdout 
log4j.appender.stdout=org.apache.log4j.ConsoleAppender 
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c]- %m%n 
log4j.appender.logfile=org.apache.log4j.FileAppender 
log4j.appender.logfile.File=target/spring.log 
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout 
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c]- %m%n

  (4)创建包名com.atguigu.zk

  (5)创建类名称zkClient

  3.2、创建ZooKeeper客户端

  //连接地址,注意:逗号左右不能有空格
    private String connectString = "hadoop102:2181,hadoop103:2182,hadoop104:2181";
    private int sessionTimeout = 2000;
    private ZooKeeper zkClient;

    @Before
    public void init() throws IOException {

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

            }
        });
    }

  3.3、创建子节点

  @Test
    public  void create() throws KeeperException, InterruptedException {
        /**
         * 参数1:要创建的节点的路径
         * 参数2:节点数据
         * 参数3:节点权限
         * 参数4:节点类型
         */
        zkClient.create("/atguigu", "ss.avi".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.CONTAINER.PERSISTENT);
    }

测试:右键run create 之后,在hadoop102的zk客户端上查看创建节点情况

#查看有哪些节点
ls /

查看节点/atguigu的内容
get -s /atguigu

 结果发现,确实创建了节点/atguigu,是存在的,并且值为 ss.avi

  3.4、获取子节点并且监听节点变化

   @Test
    public void getChildren() throws KeeperException, InterruptedException {
        List<String> children = zkClient.getChildren("/", true);

        for (String child : children) {
            System.out.println(child);
        }
    }

  运行后在IDEA控制台上看到如下节点

  hadoop102主机上zk客户端上查看根路径下有几个节点

 

   两边一致,一模一样

   在hadoop102的客户端上再创建一个节点/atguigu1,观察IDEA控制台是否有变化

create /atguigu1 "atguigu1

 

   结果发现,IDEA控制台上没有任何变化,也就是说没有监听到相关变化

  原因是:该程序已经结束,未监听到变化

  解决办法:加上如下一行代码,延时阻塞

// 延时阻塞
Thread.sleep(Long.MAX_VALUE);

  重新运行该方法,IDEAL控制台结果如下

   发现,之前的节点/atguigu1打印出来了,并且程序一直在运行中。

  现在测试一下它是否可以监听到节点的变化,我们在hadoop102上再次创建一个新的节点/atguigu2,并观察IDEA控制台是否有变化

 

 

   结果发现节点虽然创建成功了,但是IDEAL控制台仍然未监听到

  原因:虽然开启了观察模式watch为true,但是我们在之前测试过,注册一次,只能监听一次。想再次监听,需要再次注册。

  解决办法:只需要将这一块代码zk客户端监听代码块里面,相当于注册完监听,用完失效之后,再次注册一次。只要上次监听结束,再开启下一次监听

   init方法最终结果

  @Before
    public void init() throws IOException {

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

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

                List<String> children = null;
                try {
                    children = zkClient.getChildren("/", true);

                    for (String child : children) {
                        System.out.println(child);
                    }

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

                } catch (KeeperException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
    }

  启动后,IDEA控制台效果如下(注意:默认是监听了两次的)

   再次测试一下它是否可以监听到节点的变化,我们在hadoop102上再次创建一个新的节点/atguigu3,并观察IDEA控制台是否有变化

 

   发现,这次确实监听到了,打印了全部节点,包括/atguigu3

  最后,我们删除节点/atguigu1和/atguigu2,观察变化

 

   ok了,IDEAL控制台监听到了节点的变化了

  3.5、判断Znode是否存在

/**
     * 判断znode是否存在
     * @throws KeeperException
     * @throws InterruptedException
     */
    @Test
    public void exist() throws KeeperException, InterruptedException {
        /**
         * 第一个参数:监听路径
         * 第二个参数:是否开启监听
         */
        Stat stat = zkClient.exists("/atguigu", false);
        System.out.println(stat==null?"not exist":"exist");
    }

  这里我们没有开启监听,所以注释掉3.4中监听的代码块

  运行后观察IDEA控制台

   显示存在

  下面,我们删除节点/atguigu,后再次运行查看IDEAL客户端

 

   节点已不存在了

4、客户端向服务器写数据流程

  文字描述:写流程请求直接发送给Leader节点

  (1)客户端Client向Leader Server发送write请求,Leader先自己写入

  (2)Leader自己写完毕之后,向集群中的一台Follower1发送write命令,Follower1也写入数据

  (3)Follower1写入完毕之后,回复(ack)Leader,告诉它自己写入完毕

  (4)此时Leader发现已经有两台服务器写入完毕了,超过了zk集群的半数机制,可以正常运行,回复(ack)客户端Client集群已经写入完毕

  (5)同时Leader会继续告诉集群中的另一台Follower2要写入数据,Follower2收到命令后也写入数据

  (6)Follower2写入完毕之后,回复(ack)Leader,告诉它自己写入完毕

   文字描述:写流程请求直接发送给Follower节点

  (1)客户端Client向Follower1发送write请求,Follower1收到命令,但是发现自己没有写入集群权限

  (2)Follower1自己没权限,把write请求转发给老大Leader,Leader收到请求后,自己先写入数据

  (3)Leader自己写入完毕后,同时发送write命令给Follower1,Follower1也写入数据

  (4)Follower1写入完毕之后,回复(ack)Leader,告诉它自己写入完毕

  (5)此时Leader发现已经有两台服务器写入完毕了,超过了zk集群的半数机制,可以正常运行,回复Follower1集群已经写入完毕。注意:这里为啥是回复通知Follower1,而不是客户端Client?因为客户端Client是向Follower1发送的write命令

  (6)Follower1收到命令后,整理回复(ack)客户端Client集群已经写入完毕

  (7)同时Leader会继续告诉集群中的另一台Follower2要写入数据,Follower2收到命令后也写入数据

  (8)Follower2写入完毕之后,回复(ack)Leader,告诉它自己写入完毕

 

四、服务器动态上下线监听案例

1、需求

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

2、需求分析

3、具体实现

  (1)现在集群上创建 /servers 节点

create /servers "servers"

  (2)在IDEA中创建包名:com.atguigu.zkcase1 

  (3)服务器端向Zookeeper注册代码

package com.atguigu.zkcase1;

import org.apache.zookeeper.*;

import java.io.IOException;

public class DistributeServer {

    private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
    private int sessionTimeout = 2000;
    private ZooKeeper zk;

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {

        DistributeServer server = new DistributeServer();

        //1、获取zk连接
        server.getConnect();
        
        //2、注册服务器到zk集群
        server.register(args[0]);
        
        //3、启动业务逻辑(睡觉)
        server.business();
    }

    /**
     * 业务逻辑
     * @throws InterruptedException
     */
    private void business() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }

    /**
     * 注册服务器到zk集群
     * @param hostname
     * @throws KeeperException
     * @throws InterruptedException
     */
    private void register(String hostname) throws KeeperException, InterruptedException {
        /**
         * 参数1:要创建的节点的路径
         * 参数2:节点数据
         * 参数3:节点权限
         * 参数4:节点类型
         */
        zk.create("/servers/",hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(hostname + "is online");
    }

    /**
     * 获取zk连接
     * @throws IOException
     */
    public void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            public void process(WatchedEvent watchedEvent) {

            }
        });
    }
}

  (4)客户端代码

package com.atguigu.zkcase1;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

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

public class DistributeClient {

    private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181,";

    private int sessionTimeOut = 2000;
    private ZooKeeper zk;

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {

        DistributeClient client = new DistributeClient();

        //1、获取zk连接
        client.getConnect();


        //2、监听/servers下面子节点的增加和删除
        client.getServerList();

        //3、业务逻辑(睡觉)
        client.business();
    }


    /**
     * 业务逻辑
     * @throws InterruptedException
     */
    private static void business() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }

    /**
     * 获取zk连接
     * @throws IOException
     */
    public void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeOut, new Watcher() {
            public void process(WatchedEvent watchedEvent) {
                //调用完毕,再次监听,达到时刻监听的目的
                try {
                    getServerList();
                } catch (KeeperException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * 获取服务器列表
     * @throws KeeperException
     * @throws InterruptedException
     */
    public void getServerList() throws KeeperException, InterruptedException {
        List<String> children = zk.getChildren("/servers", true);
        ArrayList<String> servers = new ArrayList<String>();
        for (String child : children) {
            byte[] data = zk.getData("/servers/" + child, false, null);
            servers.add(new String(data));
        }
        //打印
        System.out.println(servers);
    }
}

4、测试

  (1)在Linux命令行操作增加减少服务器

    (a)启动DistributeClient客户端

    (b)在hadoop102上zk的客户端 /servers 目录上创建临时带序号节点

create -e -s /servers/hadoop101 "hadoop101"

   看DistributeClient客户端的控制台是否监听到新的服务器节点

   发现监听到节点上线了,依次再添加另外两个节点

create -e -s /servers/hadoop102 "hadoop102"

create -e -s /servers/hadoop103 "hadoop103"

   (c)执行删除操作

ls /servers

delete /servers/hadoop1030000000002

delete /servers/hadoop1020000000001

delete /servers/hadoop1010000000000

   (2)在IDEAL上操作增加减少服务器

    (a)启动 DistributeClient 客户端(如果已经启动过,不需要重启)
    (b)启动 DistributeServer 服务
      ①点击 Edit Configurations
      ②在弹出的窗口中(Program arguments)输入想启动的主机,例如,hadoop102

      ③回到 DistributeServer 的 main 方 法 , 右 键 , 在 弹 出 的 窗 口 中 点 击 Run“DistributeServer.main()”

      ④观察 DistributeServer 控制台,提示 hadoop102 is working

      ⑤观察 DistributeClient 控制台,提示 hadoop102 已经上线

 

五、ZooKeeper分布式锁案例

  什么叫做分布式锁呢?

  比如说“进程1”在使用该资源的时候,会先去获得锁,“进程1”获得锁之后会对该资源保持独占,这样其他进程就无法访问该资源,“进程1”用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫做分布式锁。

 1、原生Zookeeper实现分布式锁案例

  (1)分布式锁实现

package com.atguigu.zkcase2;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

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

public class DistributedLock {

    private final String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
    private final int sessionTimeOut = 2000;
    private final ZooKeeper zk;

    private CountDownLatch countDownLatch = new CountDownLatch(1);
    private  CountDownLatch waitLatch = new CountDownLatch(1);

    private String waitPath;
    private String currentNode;

    public DistributedLock() throws IOException, InterruptedException, KeeperException {

        //1、获取连接
        zk = new ZooKeeper(connectString, sessionTimeOut, new Watcher() {
            public void process(WatchedEvent watchedEvent) {
                //countDownLatch 如果连接上zk 可以释放
                if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
                    countDownLatch.countDown();
                }
                //waitLatch  需要释放
                if(watchedEvent.getType() == Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){
                    waitLatch.countDown();
                }
            }
        });

        //等待zk正常连接之后,往下走程序
        countDownLatch.await();

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


    /**
     * 对zk加锁
     */
    public void zkLock(){
        try {
            //创建对应的临时带序号节点
            currentNode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            //判断创建的节点是不是最小的序号节点,如果是获取到锁;如果不是,监听它序号前一个节点
            List<String> children = zk.getChildren("/locks", false);

            //如果children只有一个值,那就直接获取锁;如果有多个节点,需要判断,谁最小
            if(children.size() == 1){
                return;
            }else{
                //排序
                Collections.sort(children);

                //获取节点名称 seq-00000000
                String thisNode = currentNode.substring("/locks/".length());
                //通过seq-00000000获取该节点在children集合的位置
                int index = children.indexOf(thisNode);
                //判断
                if(index == -1){
                    System.out.println("数据异常");
                }else if(index == 0){
                    //就一个节点,可以获取锁了
                    return;
                }else{
                    //需要监听 他前一个节点的变化
                    waitPath = "/locks/"+children.get(index - 1);
                    zk.getData(waitPath, true, null);

                    //等待监听
                    waitLatch.await();
                    return;
                }
            }

        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    /**
     * 解锁
     */
    public void unZkLock() throws KeeperException, InterruptedException {
        //删除节点
        zk.delete(currentNode,-1);
    }
}

  (2)分布式测试

  (a)创建两个线程

package com.atguigu.zkcase2;

import org.apache.zookeeper.KeeperException;

import java.io.IOException;

public class DistributedLockTest {

    public static void main(String[] args) throws InterruptedException, IOException, KeeperException {

        final DistributedLock lock1 = new DistributedLock();

        final DistributedLock lock2 = new DistributedLock();

        new Thread(new Runnable() {
            public void run() {
                try {
                    lock1.zkLock();
                    System.out.println("线程1 启动,获取到锁");
                    Thread.sleep(5*1000);

                    lock1.unZkLock();
                    System.out.println("线程1 释放锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (KeeperException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        new Thread(new Runnable() {
            public void run() {
                try {
                    lock2.zkLock();
                    System.out.println("线程2 启动,获取到锁");
                    Thread.sleep(5*1000);

                    lock2.unZkLock();
                    System.out.println("线程2 释放锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (KeeperException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

  (b)观察控制台变化

 2、Curator框架实现分布式锁案例

  (1)原生的Java APT开发存在的问题

    (a)会话连接是异步的,需要自己去处理。比如使用CountDownLatch

    (b)Watch需要重复注册,不然就不能生效

    (c)开发的复杂性还是比较高

    (d)不支持多节点删除和创建。需要自己去递归

  (2)Curator是一个专门解决分布式锁的框架,解决了原生Java APT开发分布式遇到的问题

      详情请查看官方文档:https://curator.apache.org/index.html

  (3)Curator案例实操

    (a)添加依赖

     <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-client</artifactId>
            <version>4.3.0</version>
        </dependency>

    (b)代码实现

package com.atguigu.zkcase3;

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 lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");

        //创建分布式锁2
        final InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");

        new Thread(new Runnable() {
            public void run() {
                try {
                    lock1.acquire();
                    System.out.println("线程1 获取到锁");

                    lock1.acquire();
                    System.out.println("线程1 再次获取到锁");

                    Thread.sleep(5*1000);

                    lock1.release();
                    System.out.println("线程1 释放锁");

                    lock1.release();
                    System.out.println("线程1 再次释放锁");

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();


        new Thread(new Runnable() {
            public void run() {
                try {
                    lock2.acquire();
                    System.out.println("线程2 获取到锁");

                    lock2.acquire();
                    System.out.println("线程2 再次获取到锁");

                    Thread.sleep(5*1000);

                    lock2.release();
                    System.out.println("线程2 释放锁");

                    lock2.release();
                    System.out.println("线程2 再次释放锁");

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }

    private static CuratorFramework getCuratorFramework() {
        ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);

        CuratorFramework client = CuratorFrameworkFactory.builder().connectString("hadoop102:2181,hadoop103:2181,hadoop104:2181")
                .connectionTimeoutMs(2000)
                .sessionTimeoutMs(2000)
                .retryPolicy(policy).build();

        //启动客户端
        client.start();
        System.out.println("zookeeper 启动成功");
        return client;

    }

}

  (c)观察控制台变化

 

 

六、企业面试真题(面试重点)

1、选举机制

  半数机制,超过半数的投票通过,即通过

  (1)第一次启动选举规则:

    投票过半数时,服务器id大的胜出

  (2)第二次启动选举规则:

    (a)EPOCH(Leader任期的id)大的直接胜出

    (b)EPOCH相同,事务id大的胜出

    (c)事务id相同,服务器id大的胜出

2、生产集群安装多少zk合适?

  安装奇数

  生产经验:

  (1)10台服务器:3台zk

  (2)20台服务器:5台zk

  (3)100台服务器:11台zk

  (4)200台服务器:11zk

  服务器台数多:好处,提高可靠性;坏处,提高通信延时

3、常用命令

  ls get create delete

 

posted @ 2021-10-26 20:01  沧海一粟hr  阅读(196)  评论(0编辑  收藏  举报