认识Zookeeper

Zookeeper是什么

概述

  • Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目

  • 工作机制:
    在这里插入图片描述

特点

在这里插入图片描述

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

数据结构

在这里插入图片描述

应用场景

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

  • 统一命名服务

    • 在分布式环境下,经常需要对应用/服务进行统一命名,便于识别
    • 例如:IP不容易记住,而域名容易记住
  • 统一配置管理

    • 分布式环境下,配置文件同步非常重要
      • 一般要求一个集群中,所有节点的配置信息时一致的,比如Kafka集群
      • 对配置文件修改后,希望能够快速同步到各个节点上
    • 配置管理可交由Zookeeper实现
      • 可将配置信息写入Zookeeper上的一个Znode
      • 各个客户端服务器监听这个Znode (使用观察者模式)
      • 一旦Znode中的数据被修改,Zookeeper将通知各个客户端服务器
  • 统一集群管理

    • 分布式环境中,实时掌握每个节点的状态是必要的
      • 可根据节点实时状态做出一些调整
    • ZooKeeper可以实现实时监控节点状态变化
      • 可将节点信息写入ZooKeeper上的一个ZNode
      • 监听这个ZNode可获取它的实时状态变化
  • 服务器动态上下线

    • 客户端能实时洞察到服务器上下线的变化
    • 详细见概述
  • 软负载均衡

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

Zookeeper安装

本地模式安装部署

  • 配置JDK
  • 拷贝Zookeeper到linux中,解压到任意目录
  • 修改配置
    • 将zookeeper目录下/conf目录中的zoo_sample.cfg复制一份改名为zoo.cfg
    • 打开zoo.cfg文件,修改dataDir路径为你想要存放zookeeper数据的路径,然后创建该文件夹
  • 操作zookeeper (在zookeeper目录下)
    • 启动zookeeper服务器:bin/zkServer.sh start
    • 查看进程是否启动jps
    • 查看状态:bin/zkServer.sh status
    • 启动客户端:bin/zkCli.sh
    • 退出客户端:quit
    • 停止服务器:bin/zkServer.sh stop

配置参数解读

  • Zookeeper中的配置文件zoo.cfg中参数含义解读如下:
    1. tickTime = 2000:通信心跳数,Zookeeper服务器与客户端心跳时间,单位毫秒

      • Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒
      • 它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)
    2. initLimit = 10:LF初始通信时限

      • 集群中的Follower跟随者服务器与Leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器连接到Leader的时限 initLimit * tickTime
    3. syncLimit = 5:LF同步通信时限

      • 集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer。
    4. dataDir

      • 主要用于保存Zookeeper中的数据
    5. clientPort = 2181:客户端连接端口监听客户端连接的端口

      • 监听客户端连接的端口

Zookeeper内部原理

选举机制

  • 半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器

  • Zookeeper虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper 工作时,是有一台主机为Leader,其他则为Follower,Leader是通过内部的选举机制临时产生的

  • 以一个简单的例子来说明整个选举的过程

    • 假设有五台服务器组成的Zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么

      • 服务器1启动,此时只有它一台服务器启动了,它发出去的报文没有任何响应,所以它的选举状态一直是LOOKING状态
      • 服务器2启动,它与最开始启动的服务器1进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以id值较大的服务器2胜出,但是由于没有达到超过半数以上的票选,选举会进行进行
      • 服务器3启动,此时1,2都会投给3一票,加上3自己投给自己的一票,一共5台服务器,此时服务器3成功得到半数以上的票选,成为Leader,选举结束,此时就算服务器4,5启动,myid大于3也无济于事
  • 每个服务器在投自己的同时会给另外的所有服务器发送自己的投票信息。这样别的服务器才知道谁是最大的myid,把票投给最大的myid。上线机器之间的通信,每次投票是投当前上线机器中id最大的那个

  • myid:服务器ID

    • 比如有三台服务器,编号分别是1,2,3。编号越大在选择算法中的权重越大
  • Zxid:数据ID

    • 服务器中存放的最大数据ID。值越大说明数据越新,在选举算法中数据越新权重越大

节点类型

  • 持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
  • 短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
  • 持久化目录节点
    • 客户端与zookeeper断开连接后,该节点依旧存在
  • 持久化顺序编号目录节点
    • 客户端与zookeeper断开连接后,该节点依旧存在,只是zookeeper给该节点名称进行了顺序编号
    • 说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
    • 注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
  • 临时目录节点
    • 客户端与zookeeper断开连接后,该节点被删除
  • 临时顺序编号目录节点
    • 客户端与zookeeper断开连接后,该节点被删除,只是zookeeper给该节点名称进行了顺序编号

Stat结构体

  • czxid - 创建节点的事务zxid
    • 每次修改Zookeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper,事务ID
    • 事务ID是Zookeeper中所有修改总的次序。每个修改都有唯一的 zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生
  • ctime - znode,被创建的毫秒数(从1970年开始)
  • mzxid - znode最后更新的事务zxid
  • mtime - znode最后修改的毫秒数(从1970年开始)
  • pZxid - znode最后更新的子节点zxid.
  • cversion - znode子节点变化号,znode子节点修改次数
  • dataversion - znode数据变化号
  • aclVersion - znode访问控制列表的变化号
  • ephemeralOwner - 如果是临时节点,这个是 znode,拥有者的 session id。如果不是临时节则是0
  • dataLength - znode的数据长度,
  • numChildren - znode子节点数量

监听器原理

在这里插入图片描述

写数据流程

在这里插入图片描述

Zookeeper实战

分布式安装部署

  • 集群规划

    • 在3个主机上部署zookeeper
  • 解压安装

  • 配置服务器编号

    • 在zookeeper根目录下创建zkData文件夹
    • zkData目录下创建myid文件
    • 编辑myid文件,添加对应编号数
    • 拷贝配置到其他主机上,注意修改编号数
  • 配置zoo.cfg文件

    • 在zookeeper根目录下的/conf目录中把zoo_sample.cfg复制一份改名为zoo.cfg

    • 打开zoo.cfg文件,修改数据存储路径为zkData所在的路径

    • 然后增加集群配置

      server.1=192.168.88.22:2888:3888
      server.2=192.168.88.22:2889:3889
      server.3=192.168.88.22:2890:3890
      
    • 拷贝配置到其他主机上

    • 配置参数server.A=B:C:D解读

      • A是一个数字,表示这是第几号服务器,集群模式下配置一个文件myid,这个文件里的数就是A的值,zookeeper启动时读取此文件,拿到里面的数据与zoo.cfg里面的配置信息比较从而判断到底是哪个server
      • B是服务器的ip地址
      • C是服务器与集群中的Leader服务器交换信息的端口
      • D是执行选举时服务器相互通信的端口
  • 集群操作

    • 分别启动zookeeper
    • 查看状态等
    • 主节点停掉在集群可用的情况下会重新选举

zookeeper集群角色

  • 在Zookeeper集群服务中有三个角色:
    • Leader领导者:
      1. 处理事务请求 (增删改)
      2. 集群内部各服务器的调度者 (同步数据)
    • Follower跟随者:
      1. 处理客户端非事务请求,转发事务请求给Leader服务器 (查)
      2. 参与Leader选举投票
    • Observer观察者 (分担follower的压力,不影响leader选举):
      1. 处理客户端非事务请求,转发事务请求给Leader服务器

客户端命令行操作

在这里插入图片描述

  • 高版本rmr命令已经替换为deleteallls2被遗弃,使用ls -s /

  • 启动客户端bin/zkCli.sh

  • 显示所有操作命令help

  • 查看当前znode中所包含的内容ls /

  • 查看当前节点详细数据

  • 分别创建2个普通节点create /test "test1"create /test/demo "test2"引号可要可不要

  • 获得节点的值get /test/demo

  • 创建短暂节点

    • create -e /test/demo1 "test"
    • 在当前客户端是能查到的ls /test
    • 退出当前客户端后再重启就查看不到了
  • 创建带序号的节点

    • 先创建一个普通的根节点/test/demo "test"
    • 创建带序号的节点create -s /test/demo "test1"create -s /test/demo "test2"
    • 如果原来没有序号节点,序号从0开始依次递增。如果原节点下已有2个节点,则再排序时从2开始,以此类推
  • 修改节点数据set /test/demo "demo"

  • 节点的值变化监听get /test watch,一次性,节点值被修改了会通知

  • 节点的子节点变化监听 (路径变化) 还是一次性

    • 一台主机ls /test watch
    • 另一台创建子节点create /test/demo "test",之前的主机会收到通知
    • ls监控节点,get监控数据
  • 删除节点delete /test/demo

  • 查看节点状态stat /test

zookeeper默认端口

  • 2181:对客户端提供服务
  • 2888:集群内主机通讯 (Leader监听此端口)使用
  • 3888:选举Leader使用

API应用

  • 导入依赖

    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.10</version>
    </dependency>
    
  • 测试

    public class TestZookeeper {
    
        private ZooKeeper zkClient;
    
        //连接集群
        @Before
        public void Init() throws IOException {
            String connectString="192.168.88.22:2181,192.168.88.22:2182,192.168.88.22:2183";
            int sessionTimeout = 2000;
            zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
                @Override
                // 监听线程,用来监听子节点
                public void process(WatchedEvent watchedEvent) {
                    List<String> children = null;
                    try {
                        children = zkClient.getChildren("/", true);
                        for (String child : children) {
                            // 获取子节点数据
                            System.out.println(child);
                        }
                    } catch (KeeperException | InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    
        //创建节点
        @Test
        public void createNode() throws KeeperException, InterruptedException {
            // ZooDefs.Ids.OPEN_ACL_UNSAFE 完全开放的ACL,任何连接的客户端都可以操作该属性znode
            // CreateMode.PERSISTENT 持久的
            String s = zkClient.create("/test", "cbc".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            System.out.println(s);
        }
        
        //获取子节点
        @Test
        public void getChildren() throws Exception {
            // 获取目录下的所有子节点并监听
            List<String> children = zkClient.getChildren("/", true);
            // 延时阻塞
            Thread.sleep(Long.MAX_VALUE);
        }
    
        //判断节点是否存在
        @Test
        public void exist() throws KeeperException, InterruptedException {
            Stat exists = zkClient.exists("/test", false);
            // 有则返回状态信息,无则返回null
            System.out.println(exists == null ? "not exist" : "exist");
        }
        
        //根据版本修改,获取时的版本必须与修改时的版本一致,不然修改失败
        @Test
        public void updateByVersion() throws KeeperException, InterruptedException {
            Stat stat = new Stat();
            zkClient.getData("/test", false, stat);
            System.out.println(stat.getVersion());
            zkClient.setData("/test", "demo".getBytes(), stat.getVersion());
        }
    
        //删除节点,根据版本删除
        @Test
        public void delete() throws KeeperException, InterruptedException {
            Stat stat = new Stat();
            zkClient.getData("/test", false, stat);
        }
        
    }
    
Curator介绍
  • Curator是Apache Zookeeper的Java客户端库

  • 常见的Zookeeper Java APl:

    • 原生Java API (复杂)
    • ZkClient (简单)
    • Curator (更简单)
  • Curator项目的目标就是简化zookeeper客户端的使用

  • Curator最初是Netfix研发的,后来捐献了Apache基金会,目前是Apache的顶级项目

  • Curator和zookeeper需要对应版本,Curator能向下兼容zookeeper版本

  • 依赖

    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>4.0.1</version>
    </dependency>
    
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>4.0.1</version>
    </dependency>
    
  • 测试

    @Test
    public void connect(){
        /*
         * @param connectString       连接字符串 "192.168.88.22:2181,192.168.88.22:2182"
         * @param sessionTimeoutMs    会话超时时间
         * @param connectionTimeoutMs 连接超时时间
         * @param retryPolicy         重试策略
         */
        // 连接失败的话每3秒重试一次,共重试10次
        ExponentialBackoffRetry retry = new ExponentialBackoffRetry(3000, 10);
        // 建立连接方式1
        /*CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.88.22:2181", 60000, 15000, retry);
        // 开启连接
        client.start();*/
        // 建立连接方式2
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("192.168.88.22:2181")
                .sessionTimeoutMs(60000)
                .connectionTimeoutMs(15000)
                .retryPolicy(retry).namespace("test").build(); // namespace设置后所以操作都会在以命名空间为根节点的情况下执行
        client.start();
    }
    
Watch事件监听
  • Zookeeper允许用户在指定节点上注册一些watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到对应的客户端上去,该机制是Zookeeper实现分布式协调服务的重要特性

  • Zookeeper中引入了watcher机制来实现发布/订阅功能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者

  • Zookeeper原生支持通过注册watcher来进行事件监听,但是其使用并不是特别方便,需要开发人员自己反复注册watcher,比较繁琐

  • Curator引入了Cache来实现对zookeeper服务端事件的监听

  • Zookeeper提供了三种watcher:

    • NodeCache:只监听某一个特定的节点

    • PathChildrenCache:监听一个节点的子节点

    • TreeCache:可以监控某个节点及其所有子节点,类似于PathChildrenCache和NodeCache的组合

    • 测试

      private CuratorFramework client;
      
      @Before
      public void connect() {
          	/*
               * @param connectString       连接字符串 "192.168.88.22:2181,192.168.88.22:2182"
               * @param sessionTimeoutMs    会话超时时间
               * @param connectionTimeoutMs 连接超时时间
               * @param retryPolicy         重试策略
               */
          // 连接失败的话每3秒重试一次,共重试10次
          ExponentialBackoffRetry retry = new ExponentialBackoffRetry(3000, 10);
          // 建立连接方式1
          /*CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.88.22:2181", 60000, 15000, retry);
              // 开启连接
              client.start();*/
          // 建立连接方式2
          client = CuratorFrameworkFactory.builder()
              .connectString("192.168.88.22:2181")
              .sessionTimeoutMs(60000)
              .connectionTimeoutMs(15000)
              // namespace模式下test节点没有子节点的话一会儿后test就会被删除
              .retryPolicy(retry).namespace("test").build(); // namespace设置后所以操作都会在以命名空间为根节点的情况下执行
          client.start();
      }
      
      @Test
      // 监听特定节点
      public void NodeCache() throws Exception {
          //1. 创建NodeCache对象
          NodeCache nodeCache = new NodeCache(client, "/app1");
          //2. 注册监听
          nodeCache.getListenable().addListener(new NodeCacheListener() {
              @Override
              public void nodeChanged() throws Exception {
                  System.out.println("节点变化了");
                  // 获取修改节点后的数据
                  byte[] data = nodeCache.getCurrentData().getData();
                  System.out.println(Arrays.toString(data));
              }
          });
          //3. 开启监听 buildInitial参数:如果设置为true,则开启监听时,加载缓存数据
          nodeCache.start(true);
      
          // 保证单元测试线程运行
          while (true) {
          }
      }
      
      @Test
      // 监听某个节点的所有子节点们
      public void PathChildrenCache() throws Exception {
          //1. 创建监听对象
          PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/app2", true);
          //2. 注册监听
          pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
              @Override
              public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
                  System.out.println("子节点变化了");
                  System.out.println(pathChildrenCacheEvent);
                  // 监听子节点的数据变更,并且拿到变更后的数据
                  //1. 获取类型
                  PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
                  //2. 判断类型是否是CHILD_UPDATED
                  if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
                      System.out.println("数据变化了");
                      byte[] data = pathChildrenCacheEvent.getData().getData();
                      System.out.println(Arrays.toString(data));
                  }
      
              }
          });
          //3. 开启监听
          pathChildrenCache.start();
      
          // 保证单元测试线程运行
          while (true) {
          }
      }
      
      @Test
      // 监听
      public void TreeCache() throws Exception {
          //1. 创建监听对象
          TreeCache treeCache = new TreeCache(client, "/app1");
          //2. 注册监听
          treeCache.getListenable().addListener(new TreeCacheListener() {
              @Override
              public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
                  System.out.println("节点变化了");
                  System.out.println(treeCacheEvent);
              }
          });
          //3. 开启监听
          treeCache.start();
      
          // 保证单元测试线程运行
          while (true) {
          }
      }
      

Zookeeper分布式锁

概念

  • 在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题
  • 但当我们的应用是分布式集群工作的情况下,属于多VM下的工作环境,跨VM之间已经无法通过多线程的锁解决同步问题
  • 那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题——这就是分布式锁

原理

  • 核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点
  1. 客户端获取锁时,在一个节点A下创建临时顺序节点
  2. 然后获取A下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除
  3. 如果发现自己创建的节点并非A所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件
  4. 如果发现比自己小的那个节点被删除,则客户端的Watcher舍会收到相应通知,此时再次判断自己创建的节点是否是A子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听

Curator实现分布式锁API

  • 在Curator中有五种锁方案:
    • InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
    • InterProcessMutex:分布式可重入排它锁
    • InterProcessReadWriteLock:分布式读写锁
    • InterProcessMultiLock:将多个锁作为单个实体管理的容器
    • lnterProcessSemaphoreV2:共享信号量

分布式锁案例 - 模拟12306售票

  • 创建三个线程

    public class LockTest {
    
        public static void main(String[] args) {
            Ticket12306 ticket12306 = new Ticket12306();
            // 创建客户端
            Thread t1 = new Thread(ticket12306, "携程");
            Thread t2 = new Thread(ticket12306, "飞猪");
            Thread t3 = new Thread(ticket12306, "去哪儿");
            t1.start();
            t2.start();
            t3.start();
        }
    
    }
    
  • public class Ticket12306 implements Runnable {
    
        private int tickets = 10; // 数据库的票数
        private InterProcessMutex lock;
    
        // 初始化锁
        public Ticket12306(){
            // 获取连接对象
            ExponentialBackoffRetry retry = new ExponentialBackoffRetry(3000, 10);
            // 连接zookeeper
            CuratorFramework client = CuratorFrameworkFactory.builder()
                    .connectString("192.168.88.22:2181")
                    .sessionTimeoutMs(60000)
                    .connectionTimeoutMs(15000)
                    .retryPolicy(retry).namespace("test").build();
            // 创建锁
            lock = new InterProcessMutex(client, "/lock");
        }
    
        @Override
        public void run() {
            while (true) {
                // 获得锁
                try {
                    lock.acquire(3, TimeUnit.SECONDS); // 3秒获取一次
                    if (tickets > 0) {
                        System.out.println(Thread.currentThread() + ":" + tickets);
                        Thread.sleep(100);
                        tickets--;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    // 释放锁
                    try {
                        lock.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值