【Zookeeper学习笔记篇】

在这里插入图片描述

一、初始Zookeeper
概念:
  • 📘 zookeeperApache Hadoop 项目下的一个子项目,是一个分布式协调服务用于管理分布式应用程序,翻译过来就是动物管理员,其目录结构是同Linux 的目录结构一样是一个树形结构。
  • 📘 zookeeper 的主要用来干嘛:
    • 📖 配置管理
    • 📖 分布式锁
    • 📖 集群管理
    • 📖 注册中心
下载: 🔗 https://zookeeper.apache.org/releases.html

在这里插入图片描述

安装:
  • 📕 安装单机版:

    • 1️⃣ 安装zookeeper 的前提需要安装JDK ,
    • 🔗 https://www.runoob.com/w3cnote/win7-linux-java-setup.html
    • 2️⃣ 将下载好的安装包上传到linux服务器,如果你使用的远程连接工具是FinalShell ,那么你可以点击底栏的倒数第二个图标就可以上传文件。
    • 3️⃣ 解压到指定的目录即可: tar -zxvf 安装包 -C /path
    • 4️⃣ 解压完成后需要进入conf 目录,修改配置文件mv zoo_sample.cfg zoo.cfg 注意,一定要修改为zoo.cfg 否则会找不到该文件。
    • 5️⃣ 然后进入zoo.cfg 文件,修改文件临时存储的位置为自己指定的位置,这里我指定在bin 目录同级
  • 在这里插入图片描述

  • 6️⃣ 最后分别启动服务端和客户端:./zkServer.sh start ./zkCli.sh

zoo.cfg配置文件参数说明:
  • ☘️ tickTime=2000 通信心跳时间,是指zookeeper 的服务端和客户端之间的通信心跳时间,如果超过了这个时间就会认为其中的一端挂掉了。
  • ☘️ initLimit=10 领导者和追随者之间初次通信的时间限制次数,这里便是 initLimit * tickTime 也就是说这里最多会等待20秒。
  • ☘️ syncLimit=5 领导者和追随者之间数据的同步时间限制次数,如果Leader 认为Follwer 超过了 syncLimit * tickTime 那么,Leader 就会认为 Follwer 已经挂掉,就会删除改节点。
  • ☘️ dataDir 安装时修改的配置文件路劲,默认是tmp 路径下,而默认路径在一段时间后就会自动清理掉,所以我们会更改文件存储的路径。
  • ☘️ clientPort 客户端的端口
zookeeper集群搭建:

💅 PS: 搭建zk集群至少3台服务器,集群数量达到一半就可以正常运行,搭建集群时最好是奇数台服务器这样能更好的体现zk的性能。

  • 1️⃣ 在安装单机版的基础上,创建一个名为myid 的文件夹放在zoo.cfg中的dataDir路径下 ,而且该文件必须叫myid ,在该文件中添加一个身份标识,如IP: 192.168.23.121 ,就需要在该文件中输入1 ,以此类推。

  • 2️⃣ 在zoo.cfg 配置文件中添加如下内容:

    #######################cluster##########################
    server.1=192.168.23.121:2182:3182
    server.2=192.168.23.122:2182:3182
    server.3=192.168.23.123:2182:3182
    
    • 参数说明:server.A=B:C:D
      • A: 表示这是第几号服务器,需要和myid 文件中的值一一对应
      • B: 每天服务器的地址
      • C: Leaderfollwers 的信息交互端口
      • D: Leader 挂了重新选举时用来通信的端口
  • 3️⃣ 启动集群:./zkServer.sh start ,查看状态./zkServer.sh status 如果你现在有三台服务器,那么当你启动第一台服务器时会出现ERROR 的提示,那是因为集群的数量还没有达到一半,所以你只需要再启动一台就不会报错了。

二、Zookeeper 命令操作:
Ⓜ️zk 的数据模型:
  • 📖 拥有树形目录的zk 是一个很有层次化的结构

在这里插入图片描述

  • 📖 zk 中的每一个节点被称为:ZNode ,每个节点都会保存自己的数据和节点信息,同时也是允许保存少量内容(1MB) 在该节点下。
  • 📖 分类:
    • 🍋 persistent 持久化节点
    • 🍋 ephemeral 临时节点 : -e
    • 🍋 persistent_sequential 持久化顺序节点: -s
    • 🍋 ephemeral_sequential 临时顺序节点: -es
Ⓜ️ 服务端常用命令:
  • 📖 启动zk 服务,./zkServer.sh start
  • 📖 查看zk 状态,./zkServer.sh status
  • 📖停止zk 服务,./zkServer.sh stop
  • 📖重启zk 服务,./zkServer.sh restart
Ⓜ️ 客户端常用命令:
  • 📖 连接zk 服务端,./zkCli.sh -server ip:port
  • 📖 断开连接,quit
  • 📖 设置节点值,set /节点path value
  • 📖 删除单个节点,delete /节点path
  • 📖删除带有子节点的节点,deleteall /节点path
  • 📖显示指定目录下的节点,ls 路径
  • 📖创建节点,create /节点path value
  • 📖创建临时节点,create -e /节点path value
  • 📖创建顺序节点,create -s /节点path value
  • 📖创建临时顺序节点,create -es /节点path value
  • 📖 获得节点值,get /节点path
  • 📖 获得帮助,help
  • 📖 查看节点详情,ls -s /节点path
    • 🍁 cZxid 节点被创建的事物ID
    • 🍁 ctime 创建时间
    • 🍁 mZxid 最后一次被更新的事物ID
    • 🍁 mtime 修改时间
    • 🍁 pZxid 子节点列表最后一次被更新的事物ID
    • 🍁cversion 子节点的版本号
    • 🍁 dataversion 数据版本号
    • 🍁 aclversion 权限版本号
    • 🍁 ephemeralOwner 用于临时节点,代表临时节点是事物ID,如果为持久节点那么ID为0
    • 🍁 dataLength 节点存储数据的长度
    • 🍁 numChildren 当前节点的子节点个数
三、JavaAPI 操作:
zk 客户端库的介绍:
  • 📖 原生JavaAPI,最难用

  • 📖 ZKClient,比原生的好点

  • 📖Curator,相比前两种最好的,是Netfix公司研发的后来捐给了Apache 基金会,目前是Apache 基金会的顶级项目

    🔗 https://curator.apache.org/index.html

Curator 常用API的操作:
  • 📖 建立连接:

    • 1️⃣ 导入依赖

      <!--curator-->
      <dependency>
          <groupId>org.apache.curator</groupId>
          <artifactId>curator-framework</artifactId>
          <version>4.0.0</version>
      </dependency>
      
      <dependency>
          <groupId>org.apache.curator</groupId>
          <artifactId>curator-recipes</artifactId>
          <version>4.0.0</version>
      </dependency>
      <!--日志-->
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>1.7.21</version>
      </dependency>
      
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>1.7.21</version>
      </dependency>
      
    • 2️⃣ 日志文件:

      log4j.rootLogger=off,stdout
      
      log4j.appender.stdout = org.apache.log4j.ConsoleAppender
      log4j.appender.stdout.Target = System.out
      log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
      log4j.appender.stdout.layout.ConversionPattern = [%d{yyyy-MM-dd HH/:mm/:ss}]%-5p %c(line/:%L) %x-%m%n
      
    • 3️⃣ : 建立连接:

      import org.apache.curator.RetryPolicy;
      import org.apache.curator.framework.CuratorFramework;
      import org.apache.curator.framework.CuratorFrameworkFactory;
      import org.apache.curator.retry.ExponentialBackoffRetry;
      import org.junit.Test;
      
      public class CuratorTest {
      
          @Before
          public void connectionTest(){
              //建立连接的两种方式
      
              // 每间隔3秒重试一次,一共重试10次
              RetryPolicy policy = new ExponentialBackoffRetry(3000,10);
      
              //1.第一种
              /**
               * @param connectString       连接信息list of servers to connect to
               * @param sessionTimeoutMs    会话超时时间session timeout
               * @param connectionTimeoutMs 连接超时时间connection timeout
               * @param retryPolicy         建立连接失败的重试策略retry policy to use
               */
              CuratorFramework client =
                      CuratorFrameworkFactory.newClient("IP:port", 60 * 1000, 15 * 1000, policy);
      			client.start();
              
              //2.第二种,通过链式编程的方式
              CuratorFramework client2 = CuratorFrameworkFactory
                      .builder()
                      .connectString("IP:port")
                      .sessionTimeoutMs(60 * 1000)
                      .connectionTimeoutMs(15 * 1000)
                      .retryPolicy(policy)
                  // 当然你还可以指定名称空间,意思就是当你创建一个节点的时候默认在节点前面添加前缀
                  	.namespace("csdn")
                      .build();
              client2.start();
          }
      }
      

      如果你和我一样使用的是云服务器,那么记得开放2181端口

  • 📖 添加节点:

    • @Test
          public void createTest1() throws Exception {
              //创建持久节点
              client.create().forPath("/app1","myapp".getBytes());
          }
      
          @Test
          public void createTest2() throws Exception {
              //创建临时顺序节点
              client.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/app2","myapp".getBytes());
          }
      
          @Test
          public void createTest3() throws Exception {
              //创建持久顺序节点
              client.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath("/app3","myapp".getBytes());
          }
      
          @Test
          public void createTest4() throws Exception {
              //创建多级持久顺序节点
              client
                      .create()
                      .creatingParentsIfNeeded() // 如果需要创建多级节点需要添加此参数
                      .withMode(CreateMode.PERSISTENT_SEQUENTIAL)
                      .forPath("/app4/children","myapp".getBytes());
          }
      
  • 📖 删除节点:

    •  @Test
          public void deleteTest1() throws Exception {
              // 删除单个
              client.delete().forPath("/app1");
          }
          @Test
          public void deleteTest2() throws Exception {
              // 删除多个
              client.delete().deletingChildrenIfNeeded().forPath("/app4");
          }
          @Test
          public void deleteTest3() throws Exception {
              // 必须删除
              client.delete().guaranteed().forPath("/app30000000003");
          }
      
  • 📖 修改节点:

    •  @Test
          public void setTest() throws Exception {
              // 修改数据
              int version = 0;
              Stat stat = new Stat();
              client.getData().storingStatIn(stat).forPath("/app1");
              version = stat.getVersion();
              System.out.println(version);
              client.setData().withVersion(version).forPath("/app1","myapp3".getBytes());
          }
      
  • 📖 查询节点:

    •  @Test
          public void getTest1() throws Exception {
              // 获取节点数据
              byte[] bytes = client.getData().forPath("/app1");
              System.out.println(new String(bytes));
          }
      
          @Test
          public void getTest2() throws Exception {
              // 获取子节点数据
              List<String> childrens = client.getChildren().forPath("/app4");
              for (String children : childrens) {
                  System.out.println(children);
              }
          }
          @Test
          public void getTest3() throws Exception {
              // 获取节点状态信息
              Stat stat = new Stat();
              client.getData().storingStatIn(stat).forPath("/app1");
              System.out.println(stat.getDataLength());
          }
      

      PS: 如果你想要获取顺序节点的内容那么你需要进行序列化

  • 📖Watch 事件监听:

  • zk 可以允许用户在指定节点上注册一些监听器(Watcher), 并且在触发特定事件时通知其它节点,这一机制就是zk 中实现分布式协调服务的重要特性。

  • 原生的API 操作监听器十分不方便,故此有了Curator,Curator 中使用Cache 来实现对zk 服务端事件的监听。

  • 种类:

    • 🍋 NodeCache : 只监听某个特定的节点
    • 🍋 PathChildrenCache: 监听一个节点下的子节点
    • 🍋 TreeCache : 监听整个树上的所以节点
  • NodeCache:

     	@Test
    public void nodeCacheTest() throws Exception {
        // 创建监听器
        NodeCache cache = new NodeCache(client,"/app",false);
        cache.getListenable().addListener(new NodeCacheListener() {
            @Override
            public void nodeChanged() throws Exception {
                System.out.println("监听到app数据变化");
                byte[] data = cache.getCurrentData().getData();
                System.out.println(new String(data));
            }
        });
        cache.start(true); // 是否初始化时缓存数据
    
        while (true){} // 为了让程序不停止
    }
    
  • PathChildrenCache

     @Test
    public void pathChildrenCacheTest() throws Exception {
        //当然你还可以传入自定义的线程池,以及是否压缩数据
        PathChildrenCache pathChildren = new PathChildrenCache(client,"/app",true);
        pathChildren.getListenable().addListener(new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
                System.out.println("监听到子节点变化了");
                //对监听的类型进行判断
                if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
                    byte[] data = event.getData().getData();
                    System.out.println("监听到的数据:" + new String(data));
                }
            }
        });
        pathChildren.start(true);
        while (true){}
    }
    
  • ThreeCache

     @Test
    public void threeCacheTest() throws Exception {
        TreeCache treeCache = new TreeCache(client, "/app");
        treeCache.getListenable().addListener(new TreeCacheListener() {
            @Override
            public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
                System.out.println("不仅可以监听自己而且还可以监听子节点");
                byte[] data = event.getData().getData();
                System.out.println(new String(data));
            }
        });
        treeCache.start();
        while (true){}
    }
    
  • 📖 分布式锁的实现:

在进行单机应用开发时在对数据并发同步时往往都是采用syncchronized 或者 Lock 进行加锁的方式取解决数据同步问题,但是在跨机器时的数据同步问题采用这种方式就不可以了,这时就需要使用分布式锁来实现数据同步。

  • 锁的分类:

    • 🌻 InterProcessSemaphoreMutex : 分布式非可重入锁
    • 🌻 InterProcessMutex : 分布式可重入锁
    • 🌻 InterProcessReadWriteLock : 分布式读写锁
    • 🌻 InterProcessMultiLock : 多锁单用
    • 🌻 InterProcessSemaphoreV2 : 共享信号量
  • 实现:

    • 模拟买票:
    import org.apache.curator.RetryPolicy;
    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;
    
    import java.util.concurrent.TimeUnit;
    
    public class Ticket12306 implements Runnable{
    
        private int ticket = 10;
        /**
         * 使用分布式锁
         */
        private InterProcessMutex lock;
    
        public Ticket12306(){
            RetryPolicy policy = new ExponentialBackoffRetry(3000,10);
            CuratorFramework client =
                    CuratorFrameworkFactory
                            .newClient("43.142.107.50:2181", 60 * 1000, 15 * 1000, policy);
            client.start();
    
            lock = new InterProcessMutex(client,"/app");
        }
    
        @Override
        public void run() {
            while (true){
                //获得锁
                try {
                    lock.acquire(3, TimeUnit.SECONDS);
                    if (ticket > 0){
                        System.out.println(Thread.currentThread() + "卖了第" + ticket + "张票");
                        ticket --;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    try {  //释放锁
                        lock.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
    
    	
    	public class SellTicket {
        public static void main(String[] args) {
            Ticket12306 ticket = new Ticket12306();
            Thread t1 = new Thread(ticket, "携程");
            Thread t2 = new Thread(ticket, "飞猪");
            t1.start();
            t2.start();
        }
    }
    
四、核心理论:
选举机制:

首次和二次选举,票数达到集群的一半以上即为Leader,此处假设五台机器

  • ✋ 首次启动时选举:

    • 每台机器都有选举自己的一票,当第一次启动时,第一台机器选举自己此时它只有一票未达到总的一半以上便不是Leader
    • 第二台机器启动,进行选举,此时会根据myid 文件中的number 进行比较大小,值小的将自己的票给值大的,假设第二台机器的值大于第一台,此时第二台拥有两票,未达到一半以上不是Leader
    • 第三台机器启动,进行选举,假设第三台的myid 中的number 大于第二台中myidnumber 那么第二台机器就会将手中的两票投给第三台机器,此时第三台机器则拥有1+2 的票数,达到一半以上则为Leader ,如果一旦确认了Leader 则其它机器便默认为follwer
  • ✋ 二次选举:

    • 此时假设第五台机器无法和Leader 保持通信了,开始第二次选举。
    • 选举时的两种情况:
      • Leader 任然存在
        • 此时第五台机器会进行重试与Leader 进行通信,如果能通信成功则与Leader 进行同步,如果不能成功则默认Leader 也挂了。
      • Leader 已经挂了
        • 这时第三台机器和第五台机器已经挂了,这1,2,4进行选举,选举的依据是:【Epoch(领导者任期编号) 、ZXID(事物ID) 、SID(服务器ID)】,根据Epoch > ZXID > SID 的规则进行选举。
zk 中的分布式锁原理:
  • 📖 核心思想:客户端想要获得锁,那么就需要创建节点,使用完锁,删除节点。
    • 1️⃣ 当客户端想要获得锁时会在对应节点创建一个临时顺序节点,使用完了以后删除该节点。
    • 2️⃣ 然后会获得该节点下的所有子节点,根据节点顺序值比较大小,最小的则会获得到锁,如果不是最小的则大的节点会在自己的前一个节点注册一个监听器,用来监听删除事件。
    • 3️⃣ 如果发现前一个节点触发了删除事件,那么会再一次进行比较谁的值最小,由最小值的节点获得锁。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值