Zookeeper学习

ZooKeeper

ZooKeeper简介

是一个基于观察者模式设计的分布式服务管理框架,它负责管理和存储大家都关心的数据,然后接收观察者的注册,一旦数据发生变化,就会通知在ZooKeeper注册的观察。 作出相应的反应。

ZooKeeper特点

ZooKeeper集群

  1. 一个Learder,多个follower组成的集群,主写从读。
  2. 集群中只要有半数以上的机器存活就可以工作,所以一般适合搭建奇数台服务器。(若此处搭建六台,挂断2或者3台,整个集群都不能工作。所以多搭建一台并没有保证高可用)
  3. 每个server的数据都保持一致,Client无论连接到那台Server,访问的数据都一样。
  4. 更新请求顺序执行,好比Server1,那个client先来那个client先被处理。
  5. 数据更新原子性,更新存在事务,要么成功,要么失败。
  6. 实时性,在一定时间范围内(同步数据需要较短时间),每个client能读到最新的数据。

ZooKeeper数据结构

ds

Zookeeper的数据结构模型和unix的文件系统类似,整体是一棵树,每个节点称作ZNode,每个ZNode可以存储数据,默认为1MB,并且每个ZNode可以通过路径作为唯一标识。

应用场景

统一命名服务
1) 统一命名服务 ,在分布式环境下,需要对域名与机器统一命名。
在这里插入图片描述
2) 统一配置管理,配置文件的同步,每个client监听该node,当配置发生变更时,各个client同步配置。
在这里插入图片描述

3)统一集群管理,客户端1号向ZNode节点注册客户端信息(如ip,是否存活等等),其他客户端在该ZNode注册,可监听到客户端1号的状态。当然,客户端1号也可以注册其他ZNode监控其他客户端信息。
4)服务器动态上下线
5)软负载均衡,让访问数最少的服务器去处理请求。

ZooKeeper选举机制

相关概念:
SID:服务器id,用来标识一台ZooKeeper集群中的机器,是唯一标识和myid一样。
ZXID:事务id,用来标识一次服务器状态的变更。在某一时候,集群中的每台机器的ZXID不一定完全一致,这和客户端与服务器的更新请求有关。客户端对ZooKeeper服务器每次写操作都有事务,所产生的id。
Epoch:每个learder任期的代号
假设有五台服务器(每台服务器持有一张票数)。分别为server1 ,myid=1。server2 myid=2。。。

服务器初始化启动

server1启动先给自己投一票,然后处于locking状态。server2启动同样先是给自己投一票。
这时,server1和server2各持一票,然后那个myid大,就把票数给myid大的服务器。
同理,server3启动,也是给自己一票,然后与server2比较,myid大,于是server2把两票给了server3。server3因为有了3票(超过总服务器一半票数)所以当选为learder,状态置为leading.server4启动自己会变成follower,状态为following。

服务器运行期间无法和Leader保持连接

当某台服务器和Leader保持不了连接,会认为Leader或者其他服务器挂掉,重新发起Leader选举。当前集权可能会处于两种状态
1)Leader存在
当该服务器试图去选举Leader时,会被告知Leader信息,对于该机器来说,只需要重新连接Leader即可。
2)Leader不存在

  • 首先比较EPOCH大的胜出
  • 若EPOCH相同,则ZXID大的胜出
  • 若ZXID相同,则服务器ID大的相同

ZooKeeper节点

节点信息
##查看根节点信息
ls -s / 

在这里插入图片描述
(1)czxid:创建节点的事务 zxid
每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所
有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之
前发生。
(2)ctime:znode 被创建的毫秒数(从 1970 年开始)
(3)mzxid:znode 最后更新的事务 zxid
(4)mtime:znode 最后修改的毫秒数(从 1970 年开始)
(5)pZxid:znode 最后更新的子节点 zxid
(6)cversion:znode 子节点变化号,znode 子节点修改次数
(7)dataversion:znode 数据变化号
(8)aclVersion:znode 访问控制列表的变化号
(9)ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是
临时节点则是 0。 (10)dataLength:znode 的数据长度
(11)numChildren:znode 子节点数量

节点类型

持久节点:客户端与服务端断开连接后,创建的节点不删除

  • 持久节点都不带序号 /Znode1
  • 持久节点带序号 /Znode2_001 /Znode3_002
    短暂节点:客户端与服务端断开连接后,创建的节点自动删除
  • 临时节点都不带序号 /Znode1
  • 临时节点带序号 /Znode2_001 /Znode3_002
 ## 创建永久节点(不带序号)不可重复创建
 create /testNode '描述信息'
 create /testNode/test111 'testNode的子节点'
 ## 创建永久节点(带序号 -s)可重复创建  响应 : Created /testNode/test2220000000001 
 create -s /testNode/test222  
 ## 创建临时节点 (带序号 -s)
 create -e /testNode/tmp
 ## 查看节点
 ls /
 ls /testNode
## 获取节点
 get /testNode/test111
 get -s /testNode/test111
## 修改节点值
 set /testNode/test111 '修改testNode的子节点'
## 删除节点
delete /testNode/test111/test11111
delteall /testNode
## 查看节点状态信息
stat /testNode/test111

监听器原理

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目
录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数
据的任何改变都能快速的响应到监听了该节点的应用程序。
1、监听原理详解
1)首先要有一个main()线程
2)在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
3)通过connect线程将注册的监听事件发送给Zookeeper。
4)在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
5)Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
6)listener线程内部调用了process()方法。
2、常见的监听
1)监听节点数据的变化

get path [watch]
## 监控 test111的节点数据
get -w /TestNode/test111

在这里插入图片描述
再次修改,就不会再监听。一次注册,一次监听。再次注册,才能再次监听
2)监听子节点增减的变化

ls path [watch] 
create /testNode/test111/test11111
ls -w /testNode/test111

在这里插入图片描述

写数据流程

1)客户端写数据请求发送到Leader
在这里插入图片描述

  1. 首先客户端发送请求值Leader接收
  2. Leader将数据自己先写一份,然后通知Follwer,让Follwer写数据
  3. Follwer写完数据,返回ack
  4. 只要有超过一半机器写完数据,Leader就会返回ack给客户端
  5. 继续通知其他Follwer写数据,其他机器写完回复ack。
    2)请求发送到Follwer
    在这里插入图片描述
  6. 请求发送到Follower,Follwer会将请求转发给Leader
  7. Leader会通知Follwer写入数据,Follower写完数据恢复ack
  8. 当有超过一半机器写完数据,Leader会回复ack给接收请求的Follwer
  9. 接收请求的Follwer回复ack给Client
  10. 最后继续让其他未完成同步操作的机器写数据,写完回复ack即完成。

代码实现

<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>
        <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>
    </dependencies>
package com.example.demo.zk;

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

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

/**
 * @author huikf
 */
public class ZookeeperClient {

    private static final String CONNECT = "127.0.0.1:2181";
    private static final int TIME_OUT = 2_000;
    private ZooKeeper zooKeeper;

    @Before
    public void init() throws IOException {
        zooKeeper = new ZooKeeper(CONNECT, TIME_OUT, (Watcher) watchedEvent -> {
            try {
                List<String> children = zooKeeper.getChildren("/", true);
                children.forEach(System.out::println);
            } catch (KeeperException | InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    @Test
    public void create() throws KeeperException, InterruptedException {
        String s = zooKeeper.create("/family", "hkf".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        System.out.println(s);
    }

    /**
     * watch参数为true,代表使用默认监听器实现,即init中的watch
     * 但也可以自己再实现一个
     * 当主线程未结束时,init中的watch一直监听变化
     */
    @Test
    public void getChildren() throws KeeperException, InterruptedException {
        List<String> children = zooKeeper.getChildren("/", true);
        children.forEach(System.out::println);

        Thread.sleep(Long.MAX_VALUE);
    }

    @Test
    public void exist() throws KeeperException, InterruptedException {
        Stat exists = zooKeeper.exists("/family", false);
        System.out.println(exists == null);
    }
}

案例

服务器动态上下线

服务端

/**
 * @author huikf
 * @since 2021/9/11 9:44
 */
public class DistributeServer {
    ZooKeeper zooKeeper;
    private static final String CONNECT = "127.0.0.1:2181";
    private static final int TIME_OUT = 2_000;

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
        DistributeServer distributeServer = new DistributeServer();
        //初始化客户端
        distributeServer.initClient();
        //注册服务器,给arges[0] 赋值
        distributeServer.register(args[0]);
        //业务处理
        distributeServer.bussiness();

    }

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

    /**
     * 创建/servers/host1
     *             /host2
     * 节点类型为虚拟,有序号的,当某台服务器挂掉,节点就会消失。
     *
     * @param host 主机
     * @author huikf
     */
    private void register(String host) throws KeeperException, InterruptedException {
        zooKeeper.create("/servers/"+host, host.getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println("host:" + host + "is online");
    }

    private void initClient() throws IOException {
        zooKeeper = new ZooKeeper(CONNECT, TIME_OUT, event -> {

        });
    }

}

客户端

/**
 * @author huikf
 * @since 2021/9/11 9:53
 */
public class DistributeClient {
    ZooKeeper zooKeeper;
    private static final String CONNECT = "127.0.0.1:2181";
    private static final int TIME_OUT = 2_000;

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
        DistributeClient distributeClient = new DistributeClient();
        distributeClient.initClient();
        //监听 /servers下面的子节点的增加或者删除
        distributeClient.getServersList();
        distributeClient.bussiness();
    }

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

    private void getServersList() throws KeeperException, InterruptedException {
        //true:代表使用初始化时默认的监听器,下行代码只监听一次,若想再次监听,须在初始化时的监听器中再次调用
        List<String> children = zooKeeper.getChildren("/servers", true);
        List<String> dataS = new ArrayList<>();
        for (String child : children) {
            byte[] data = zooKeeper.getData("/servers/" + child, false, null);
            dataS.add(new String(data));
        }
        System.out.println(dataS);
    }


    private void initClient() throws IOException {
        zooKeeper = new ZooKeeper(CONNECT, TIME_OUT, event -> {
            try {
                getServersList();
            } catch (KeeperException | InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

测试

 ## 增加/servers下节点或者删除节点 客户端均可监听到
 create -e -s /servers/server1 'servers1'
 create -e -s /servers/server2 'servers2'
 create -e -s /servers/server3 'server3'
分布式锁

分布式锁

package com.example.demo.zk;

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

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

/**
 * @author huikf
 * @since 2021/9/13 9:59
 */
public class DistributedLock {
    /**
     * zookeeper server 列表
     */
    private String CONNECT = "127.0.0.1:2181";
    /**
     * 超时时间
     */
    private int TIME_OUT = 2000;
    private ZooKeeper zooKeeper;
    private String rootNode = "locks";
    private String subNode = "seq-";
    /**
     * 当前 client 等待的子节点
     */
    private String waitPath;
    /**
     * ZooKeeper 连接
     */
    private CountDownLatch connectLatch = new CountDownLatch(1);
    /**
     * ZooKeeper 节点等待
     */
    private CountDownLatch waitLatch = new CountDownLatch(1);
    /**
     * 当前 client 创建的子节点
     */
    private String currentNode;

    /**
     * 和 zk 服务建立连接,并创建根节点
     */
    public DistributedLock() throws IOException,
            InterruptedException, KeeperException {
        zooKeeper = new ZooKeeper(CONNECT, TIME_OUT, event -> {
            // 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
            if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
                connectLatch.countDown();
            }
            // 发生了 waitPath 的删除事件
            if (event.getType() == Watcher.Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
                waitLatch.countDown();
            }
        });
        // 等待连接建立
        connectLatch.await();
        //获取根节点状态
        Stat stat = zooKeeper.exists("/" + rootNode, false);
        //如果根节点不存在,则创建根节点,根节点类型为永久节点
        if (stat == null) {
            System.out.println("根节点不存在");
            zooKeeper.create("/" + rootNode, new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    /**
     * 加锁方法
     */
    public void zkLock() {
        try {
            //在根节点下创建临时顺序节点,返回值为创建的节点路径
            currentNode = zooKeeper.create("/" + rootNode + "/" + subNode,
                    null,
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            // wait 一小会, 让结果更清晰一些
            Thread.sleep(10);
            // 注意, 没有必要监听"/locks"的子节点的变化情况
            List<String> childrenNodes = zooKeeper.getChildren("/" + rootNode, false);
            // 列表中只有一个子节点, 那肯定就是 currentNode , 说明client 获得锁
            if (childrenNodes.size() == 1) {
                return;
            } else {
                //对根节点下的所有临时顺序节点进行从小到大排序
                Collections.sort(childrenNodes);
                //当前节点名称
                String thisNode = currentNode.substring(("/" + rootNode + "/").length());
                //获取当前节点的位置
                int index = childrenNodes.indexOf(thisNode);
                if (index == -1) {
                    System.out.println("数据异常");
                } else if (index == 0) {
                    // index == 0, 说明 thisNode 在列表中最小, 当前client 获得锁
                } else {
                    // 获得排名比 currentNode 前 1 位的节点
                    this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);
                    // 在 waitPath 上注册监听器, 当 waitPath 被删除时,zookeeper 会回调监听器的 process 方法
                    zooKeeper.getData(waitPath, true, new Stat());
                    //进入等待锁状态
                    waitLatch.await();
                }
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解锁方法
     */
    public void zkUnlock() {
        try {
            zooKeeper.delete(this.currentNode, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }

}

测试:

package com.example.demo.zk;

import org.apache.zookeeper.KeeperException;

import java.io.IOException;

public class DistributedLockTest {
    public static void main(String[] args) throws
            InterruptedException, IOException, KeeperException {
        // 创建分布式锁 1
        final DistributedLock lock1 = new DistributedLock();
        // 创建分布式锁 2
        final DistributedLock lock2 = new DistributedLock();
        new Thread(() -> {
            // 获取锁对象
            try {
                lock1.zkLock();
                System.out.println("线程 1 获取锁");
                Thread.sleep(5 * 1000);
                lock1.zkUnlock();
                System.out.println("线程 1 释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            // 获取锁对象
            try {
                lock2.zkLock();
                System.out.println("线程 2 获取锁");
                Thread.sleep(5 * 1000);
                lock2.zkUnlock();
                System.out.println("线程 2 释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }
}
Curator 框架实现分布式锁案例

1)原生的 Java API 开发存在的问题
(1)会话连接是异步的,需要自己去处理。比如使用 CountDownLatch
(2)Watch 需要重复注册,不然就不能生效
(3)开发的复杂性还是比较高的
(4)不支持多节点删除和创建。需要自己去递归
2)Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。
详情请查看官方文档:https://curator.apache.org/index.html

代码实现

/**
 * @author huikf
 * @since 2021/9/13 10:59
 */
public class CuratorLockTest {

    public static void main(String[] args) {
        InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
        InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");
        new Thread(() -> {
            try {
                lock1.acquire();
                System.out.println("线程一获取到锁");
                //锁重入
                lock1.acquire();
                System.out.println("线程一再次获取到锁");
                Thread.sleep(5_000);
                lock1.release();
                System.out.println("线程一释放锁");
                lock1.release();
                System.out.println("线程一再次释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();


        new Thread(() -> {
            try {
                lock2.acquire();
                System.out.println("线程二获取到锁");
                lock2.acquire();
                System.out.println("线程二再次获取到锁");
                Thread.sleep(5_000);
                lock2.release();
                System.out.println("线程二释放锁");
                lock2.release();
                System.out.println("线程二再次释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        
    }

    private static CuratorFramework getCuratorFramework() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(2_000, 3);
        CuratorFramework client = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181")
                .connectionTimeoutMs(2_000)
                .sessionTimeoutMs(2_000)
                .retryPolicy(retryPolicy)
                .build();
        client.start();
        return client;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值