ZooKeeper学习笔记

为什么会有ZooKeeper

https://zhuanlan.zhihu.com/p/69114539?utm_source=wechat_session

ZooKeeper架构

https://blog.csdn.net/qq_43161674/article/details/106380609

ZooKeeper命令

服务端

命令操作
zkServer.sh start启动服务器
zkServer.sh stop关闭服务器
zkServer.sh status查看服务器状态
zkServer.sh restart重启服务器

客户端

不记得命令了可以输入help

基本命令

命令操作
zkCli默认连接本机zk
zkCli -server localhost:2181连接指定地址zk
quit退出连接
ls /查看根目录
ls /ZooKeeper/config查看某个具体的内容
ls -s /查看子节点和相关信息

节点增删改查

创建结点并写入数据:

create [-s] [-e] path data ,其中 -s 为有序结点,-e 临时结点(默认是持久结点),不能重复创建节点

create /hadoop "123456"  # 此时,如果quit退出后再./ZkCient.sh 登入
get /hadoop              # 再用输入 get /hadoop 获取,结点依然存在(永久结点)
				       
create -s /a "a"         # 创建一个持久化有序结点,创建的时候可以观察到返回的数据带上了一个id       
create -s /b "b"         # 返回的值,id递增了

create -s -e /aa "aa"    # 依然还会返回自增的id,quit后再进来,继续创建,id依然是往后推的

create /aa/xx            # 继续创建结点,只不过没数据,可以用set命令设置,可以看到pZxid变化了

查询节点:

get /hadoop 查看结点的数据和属性,stat /hadoop 查看结点的属性

更新节点:

set path [version]

set /hadoop "345"        # 修改结点值

set /hadoop "hadoop-x" 1 # 也可以基于版本号进行更改,类似于乐观锁,当传入版本号(dataVersion)
                         # 和当前结点的数据版本号不一致时,ZooKeeper会拒绝本次修改

删除节点:

delete path [version] ,和 set 方法相似,也可以传入版本号。要想删除某个结点及其所有后代结点,可以使用递归删除,命令为 rmr path

delete /hadoop           # 删除结点
delete /hadoop 1         # 乐观锁机制,与set 方法一致

监听器

命令:get path [watch] | stat path [watch]

使用get path [watch] 注册的监听器能够在结点内容发生改变的时候,向客户端发出通知。需要注意的是ZooKeeper的触发器是一次性的(One-time trigger),即触发一次后就会立即失效

get /hadoop watch        # get 的时候添加监听器,当值改变的时候,监听器返回消息
set /hadoop 45678        # 测试

使用 ls path [watch] 或 ls -s path [watch]注册的监听器能够监听该结点下所有子节点增加删除操作

ls /hadoop watch         # 添加监听器
set /hadoop/node "node"

JavAPI操作

常见的无非是zkClient和Curator,ZooKeeper作者对Curator做出了极高评价,同时Curator是Apache的顶级项目,而ZooKeeper才只是一级项目,所以直接冲Curator就OK了

建立连接

@SpringBootTest
class SpringbootTestApplicationTests {

    @Test
    void testConnect() {
        //超时重试策略,这个是指定间隔时间的重试策略
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
//        //第一种:建立连接
//        CuratorFramework client = CuratorFrameworkFactory.newClient(
//                "localhost:2181",//zk地址,多个地址用逗号分开
//                60*1000,//会话超时时间
//                15*1000,//连接超时时间
//                retryPolicy//重试策略
//        );
//        client.start();//运行客户端

        //第二种:建立连接
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("localhost:2181")
                .sessionTimeoutMs(60*1000)
                .connectionTimeoutMs(15*1000)
                .retryPolicy(retryPolicy)
                .namespace("zrl")//默认这个是根目录,创建的时候会在前面自动加上这个
                .build();
        client.start();
        client.close
    }
}

创建节点

    @Test
    public void testCreate() throws Exception {
        // 基本创建,默认值为当前客户端ip地址
        String path = client.create()
                .forPath("/app2");
        System.out.println(path);
    }

    @Test
    public void testCreate1() throws Exception {
        // 创建节点,带数据
        String path = client.create()
                .forPath("/app2", "haha".getBytes(StandardCharsets.UTF_8));
        System.out.println(path);
    }

    @Test
    public void testCreate2() throws Exception {
        // 设置节点类型 (默认:持久化)
        String path = client.create()
                .withMode(CreateMode.EPHEMERAL) // 设置临时模式
                .forPath("/app3");
        System.out.println(path);
    }

    @Test
    public void testCreate3() throws Exception {
        // 创建多节点
        String path = client.create()
                .creatingParentContainersIfNeeded() // 父节点不存在,则创建父节点
                .forPath("/app4/p2");
        System.out.println(path);
    }

查询节点

	@Test
    public void testGet() throws Exception {
        byte[] data = client.getData().forPath("/app1");
        System.out.println(new String(data));
    }

    @Test
    public void testGet1() throws Exception {
        // 查询子节点
        List<String> path = client.getChildren().forPath("/app4");
        System.out.println(path);
    }

    @Test
    public void testGet2() throws Exception {
        // 查询节点状态 ls -s
        Stat stat = new Stat();
        client.getData().storingStatIn(stat).forPath("/app1");
        System.out.println(stat);
    }

修改节点

    @Test
    public void testSet() throws Exception {
        client.setData().forPath("/app2", "哈哈".getBytes(StandardCharsets.UTF_8));
    }

    /**
     * @author Wei
     * @date 2021/7/23 11:18
     * @description 根据版本修改数据
     **/
    @Test
    public void testSetForVersion() throws Exception {
        Stat stat = new Stat();
        client.getData().storingStatIn(stat).forPath("/app2");

        client.setData()
                .withVersion(stat.getVersion()) // 版本号
                .forPath("/app2", "哈哈123".getBytes(StandardCharsets.UTF_8));
    }

删除节点

    @Test
    public void testDelete() throws Exception {
        // 删除单个节点
        client.delete().forPath("/app1");
    }

    @Test
    public void testDelete1() throws Exception {
        // 删除带子节点的节点
        client.delete().deletingChildrenIfNeeded().forPath("/app4");
    }

    @Test
    public void testDelete3() throws Exception {
        // 必须删除成功
        client.delete().guaranteed().forPath("/app2");
    }

    @Test
    public void testDelete4() throws Exception {
        // 回调
        client.delete()
                .guaranteed()
                .inBackground((curatorFramework, curatorEvent) -> {
                    System.out.println("删除成功...");
                    System.out.println(curatorEvent);
                })
                .forPath("/app2");
    }

Watch监视器

Watcher机制实现了发布订阅功能,能够让多个订阅者同时监听某一对象,当对象自身状态变化时,会通知所有的订阅者

Curator引入了Cache实现对ZooKeeper服务端事件的监听,ZooKeeper提供了三种Watcher:

  • NodeCache:只是监听某一个特定的节点
  • PathChildrenCache:监控一个ZNode的子节点
  • TreeCache:监控整个树的所有节点,类似于前两个组合
    //可以监听当前节点和子节点(子节点的子节点)的创建、更新、删除
	@Test
    public void testNodeCache() throws Exception {
        CuratorCache curatorCache = CuratorCache.build(client, "/zrl/app1");
        curatorCache.listenable().addListener(
                new CuratorCacheListener() {
                    // 第一个参数:事件类型(枚举),节点更新删除等
                    // 第二个参数:节点更新前的状态、数据
                    // 第三个参数:节点更新后的状态、数据
                    @Override
                    public void event(Type type, ChildData childData, ChildData childData1) {
                        System.out.println(type.name());
                        System.out.println(new String(childData.getData()));
                        System.out.println(new String(childData1.getData()));
                    }
                }
        );
        // 开启监听
        curatorCache.start();
        // 因为是测试环境,需要创建死循环防止连接断开
        while (true) {

        }
    }
	//老版本的创建方式
	@Test
    public void testNodeCache1() throws Exception {
        //1.创建NodeCache对象
        NodeCache nodeCache = new NodeCache(client, "/zrl/app1");
        //2.注册监听
        nodeCache.getListenable().addListener(new NodeCacheListener() {
            @Override
            public void nodeChanged() throws Exception {
                byte[] data = nodeCache.getCurrentData().getData();
                System.out.println("修改后的数据"+new String(data));
            }
        });
        //true表示运行时就拉取数据
        nodeCache.start(true);
        while (true) {
            
        }
    }

ZooKeeper分布式锁

跨机器的进程之间的数据同步问题

ZooKeeper分布式锁原理

核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点

  • 客户端获取锁时,在lock节点下创建临时顺序节点
  • 然后客户端获取lock下的所有子节点,客户端进行判断
    • 如果发现自己创建的节点上所有子节点中最小的,则表示客户端拿到了锁,使用完锁之后,将该节点删除
    • 如果不是最小的,表示还没获取到锁,此时客户端需要找到比自己小的前一个节点,同时对其注册事件监听器,监听删除事件(因为拿到锁的客户端删除后会通知)
      • 比自己小的节点删除后,Watcher收到通知,此时判断自己创建的节点是不是lock子节点中最小的,如果是…,如果不是…,循环上面的步骤

Curator实现分布式锁

在Curator中有五种锁方案:

  • InterprocessSemaphoreMutex:分布式排它锁
  • InterProcessMutex:分布式可重入排它锁
  • InterProcessReadWhiteLock:分布式读写锁
  • Inter ProcessMultiLock:将多个锁作为单个实体管理的容器
  • InterProcessSemaphoreV2:共享信号量

就把它当作锁直接用就好了,先获取锁,再释放锁,构造的时候一般需要传入client和path,使用时会自动创建一个lock根节点

ZooKeeper集群

集群角色

ZooKeeper节点的4种状态:

  • LEADING:说明此节点已经是leader节点,处于领导者地位的状态,差不多就是一般集群中的master。但在ZooKeeper中,只有leader才有写权限,其他节点(FOLLOWING)是没有写权限的,可以读
  • LOOKING:选举中,正在寻找leader,即将进入leader选举流程中
  • FOLLOWING:跟随者,表示当前集群中的leader已经选举出来了,主要具备以下几个功能点
    • 向leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息)
    • 接收leader消息并进行处理;
    • 接收client发送过来的请求,如果为写请求,会发送给Leader进行投票处理,然后返回client结果。
  • OBSERVING:OBSERVING和FOLLOWING差不多,但不参加投票和选举,接受leader选举后的结果

投票过程

选举分为两种情况,初始化和leader挂掉的时候,要进行leader选举,至少需要2台机器,集群机器台数基本是奇数。根据服务器ID(myid)和数据ID(zxid)选举,数据ID越大表示用的这个机器越多,当投票超过半数时就选出了leader

初始化

当启动初始化集群的时候,server1的myid为1,zxid为0 server2的myid为2,zxid同样是0,以此类推。此种情况下zxid都是为0。先比较zxid,再比较myid

  • 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking(选举状态)。
  • 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的myid大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。
  • 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的myid最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。
  • 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的myid大,但之前服务器3已经胜出,所以服务器4只能成为小弟。
  • 服务器5启动,后面的逻辑同服务器4成为小弟

当选举机器过半的时候,已经选举出leader后,后面的就跟随已经选出的leader,所以4和5跟随成为leader的server3

所以,在初始化的时候,一般到过半的机器数的时候谁的myid最大一般就是leader

运行期间

按照上述初始化的情况,server3成为了leader,在运行期间处于leader的server3挂了,那么非Observer服务器server1、server2、server4、server5会将自己的节点状态变为LOOKING状态

  • 开始进行leader选举。现在选举同样是根据myid和zxid来进行
  • 首先每个server都会给自己投一票竞选leader。假设server1的zxid为123,server2的zxid为124,server4的zxid为169,server5的zxid为188
  • 同样先是比较zxid再比较,server1、server2、server4比较server4根据优先条件选举为leader。然后server5还是跟随server4,即使server5的zxid最大,但是当选举到server4的时候,机器数已经过半。不再进行选举,跟随已经选举的leader

ZooKeeper集群为保证数据的一致性所有的操作都是由leader完成,之后再由leader同步给follower。重点就在这儿,ZooKeeper并不会确保所有节点都同步完数据,只要有大多数节点(即n/2+1)同步成功即可。

咱们假设有一个写操作成功那么现在数据只存在于节点leader,之后leader再同步给其他follower。这时候宕掉3个机器,已经过半的机器无法进行投票选举,剩余2台不足过半,无法选举=无法提供任何服务。再启动一个机器恢复服务。所以宕掉的机器不要过半,过半就会导致无法正常服务。

在leader选举的时候会有30s-120s的过程,在这期间也是无法提供服务的。如果用ZooKeeper要作为服务发现是个弊端,基本无法忍受,ZooKeeper本身是一个CP系统,保证数据的一致性,在恢复的时候再提供服务,并没有多好高可用的方案。如果leader发生故障选举时无法提供服务发现对一个大型应用来说可能是致命的。它可以为同在一个分布式系统中的其他服务提供:统一命名服务、配置管理、分布式锁服务、集群管理等功能)是个伟大的开源项目,很成熟

集群搭建

用到后再在网上看吧。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笼中小夜莺

嘿嘿嘿,请用金钱尽情地蹂躏我吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值