【JAVA】Zookeeper 浅谈

目录

Zookeeper 简介

 Zookeeper 安装

Zookeeper集群搭建

Java集成ZooKeeper 

Zookeeper 监听节点

Zookeeper 实现分布锁

Zab协议


Zookeeper 简介

  • ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

  • ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
  • ZooKeeper包含一个简单的原语集,提供Java和C的接口。代码版本中,提供了分布式独享锁、选举、队列的接口,其中分布锁和队列有Java和C两个版本

 Zookeeper 安装

# 查看本地镜像
docker images
# 搜索ZooKeeper 镜像
docker search zookeeper
# 拉取ZooKeeper镜像最新版本
docker pull zookeeper:latest
# 自己使用的版本
docker pull zookeeper:3.5.7

创建docker的挂载在本地的目录

mkdir -p /mydata/zookeeper/data # 数据挂载目录
mkdir -p /mydata/zookeeper/conf # 配置挂载目录
mkdir -p /mydata/zookeeper/logs # 日志挂载目录

启动zookeeper

docker run -d --name zookeeper --privileged=true -p 2181:2181  -v /mydata/zookeeper/data:/data -v /mydata/zookeeper/conf:/conf -v /mydata/zookeeper/logs:/datalog zookeeper:3.5.7

参数说明

-e TZ="Asia/Shanghai" # 指定上海时区 
-d # 表示在一直在后台运行容器
-p 2181:2181 # 对端口进行映射,将本地2181端口映射到容器内部的2181端口
--name # 设置创建的容器名称
-v # 将本地目录(文件)挂载到容器指定目录;
--restart always #始终重新启动zookeeper,看需求设置不设置自启动

添加ZooKeeper配置文件,在刚刚配置的文件目录(/mydata/zookeeper/conf)下,新增zoo.cfg 配置文件(没有则需配置),配置内容如下:

dataDir=/data  # 保存zookeeper中的数据
clientPort=2181 # 客户端连接端口,通常不做修改
dataLogDir=/datalog
tickTime=2000  # 通信心跳时间
initLimit=5    # LF(leader - follower)初始通信时限
syncLimit=2    # LF 同步通信时限
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
maxClientCnxns=60
standaloneEnabled=true
admin.enableServer=true
server.1=localhost:2888:3888;2181
常用的操作指令
# 进入zookeeper 容器内部
docker exec -it zookeeper /bin/bash
# 检查容器状态
docker exec -it zookeeper /bin/bash ./bin/zkServer.sh status
# 进入控制台
docker exec -it zookeeper zkCli.sh

Zookeeper集群搭建

ZooKeeper集群服中务中有三个角色:

  • Leader (领导者) :1. 处理事务请求 2. 集群内部各服务器的调度者
  • Follower (跟随者) :1. 处理客户端非事务请求,转发事务请求给Leader服务器 2. 参与Leader选举投票
  • Observer (观察者):1. 处理客户端非事务请求,转发事务请求给Leader服务器。

Leader选举:

  • Serverid:服务器ID  :比如有三台服务器,编号分别是1,2,3。编号越大在选择算法中的权重越大
  • Zxid:数据ID :服务器中存放的最大数据ID.值越大说明数据越新,在选举算法中数据越新权重越大
  • 在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票,则此ZooKeeper就可以成为Leader了

搭建要求

真实的集群是需要部署在不同的服务器上的,但是在测试时同时启动很多个虚拟机内存会吃不消,所以我们通常会搭建伪集群,也就是把所有的服务都搭建在一台虚拟机上,用端口进行区分。我们这里搭建一个三个节点的Zookeeper集群(伪集群)

Java集成ZooKeeper 

导入依赖

        
            org.apache.curator
            curator-framework
            4.0.0
        
 
        
            org.apache.curator
            curator-recipes
            4.0.0
        
package eample;

import lombok.extern.slf4j.Slf4j;
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.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.junit.Before;
import org.junit.Test;

import java.util.List;

@Slf4j
public class Zookeeper {
    private CuratorFramework client;

    /**
     * zookeeper连接启动
     */
    @Before
    public void start() {
        //重试策略
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);//失败后休息时间,重试次数
        client = CuratorFrameworkFactory.builder()
                .connectString("120.****.58:2181")//连接地址
                .sessionTimeoutMs(60 * 1000)//会话超时时间 单位ms
                .connectionTimeoutMs(15 * 1000)//连接超时时间 单位ms
                .retryPolicy(retryPolicy)//重试策略
                .namespace("chw")//名称空间
                .build();
        //开启连接
        client.start();

    }

    /**
     * 创建节点:create 持久、临时、顺序 给节点设置数据
     * 1、基本创建:create().forPath("");
     * 2、创建节点,带有数据:create().forPath("",data);
     * 3、设置节点的类型:create().withMode(CreateMode.EPHEMERAL).forPath("",data);
     * 4、创建多级节点 /app1/p1:create().creatingParentsIfNeeded().forPath("",data);
     */
    @Test
    public void create() throws Exception {
        //1、基本创建
        //如果创建节点、没有指定数据,则默认将当前客户端的ip作为数据存储
        String forPath = client.create().forPath("节点一");
        System.out.println(forPath);
        //如果创建带数据的节点
        forPath = client.create().forPath("节点一","测试节点一".getBytes());
        System.out.println(forPath);
        //如果创建带数据的节点,临时目录节点, 一旦创建这个节点当会话结束, 这个节点会被自动删除
        forPath = client.create().withMode(CreateMode.EPHEMERAL).forPath("节点一","测试节点一".getBytes());
        System.out.println(forPath);
        //创建多级节点 /app1/p1,creatingParentsIfNeeded():如果父节点不存在,则创建父节点
        forPath = client.create().creatingParentsIfNeeded().forPath("/节点2/p1");
        System.out.println(forPath);
    }

    /**
     * 查询节点:
     * 1、查询数据:get:getData().forPath("");
     * 2、查询子节点:ls:getChildren().forPath("/");
     * 3、查询节点状态信息:ls -s:getData().storingStatIn(状态对象).forPath("");
     */
    @Test
    public void select() throws Exception {
        //1、查询数据
        byte[] data = client.getData().forPath("/app1");
        System.out.println(new String(data));
        //2、查询子节点:
        List forPath = client.getChildren().forPath("/");
        System.out.println(forPath);
        //3、查询节点状态信息
        Stat status = new Stat();
        client.getData().storingStatIn(status).forPath("");
        System.out.println(status);
    }


    /**
     * 修改节点数据
     * 1、修改数据:setData().forPath()
     * 2、根据版本修改:setData().withVersion().forPath()
     * version 是通过查询出来的。目的就是为了让其他客户端或者线程不干扰我。
     * @throws Exception
     */
    @Test
    public void update() throws Exception {
        //修改数据
        client.setData().forPath("/节点一","itlovo".getBytes());
        Stat status = new Stat();
        //根据版本修改
        client.getData().storingStatIn(status).forPath("/节点一");
        int version = status.getVersion();
        System.out.println(version);
        client.setData().withVersion(version).forPath("/节点一","hehe".getBytes());

    }


    /**
     * 删除节点:delete
     * 1、删除单个节点:delete().forPath("/app1");
     * 2、删除带有子节点的节点:delete().deletingChildrenIfNeeded().forPath("/app1");
     * 3、必须成功的删除:为了防止网络抖动。本质就是重试。  client.delete().guaranteed().forPath("/app2");
     * 4、回调:inBackground
     * @throws Exception
     */
    @Test
    public void delete() throws Exception{
        //1、删除单个节点
        client.delete().forPath("/节点一");
        //2、删除带有子节点的节点
        client.delete().deletingChildrenIfNeeded().forPath("/节点一");
        //3、必须成功的删除
        client.delete().guaranteed().forPath("/节点一");
        //4、删除回调
        client.delete().guaranteed().inBackground((curatorFramework, curatorEvent) -> {
            System.out.println("我被删除了~");
            System.out.println(curatorEvent);
        }).forPath("/节点一");
    }



}

Zookeeper 监听节点

package eample;

import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.junit.Before;
import org.junit.Test;

@Slf4j
public class ZookeeperListener {
    private CuratorFramework client;

    /**
     * zookeeper连接启动
     */
    @Before
    public void start() {
        //重试策略
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);//失败后休息时间,重试次数
        client = CuratorFrameworkFactory.builder()
                .connectString("120.****.58:2181")//连接地址
                .sessionTimeoutMs(60 * 1000)//会话超时时间 单位ms
                .connectionTimeoutMs(15 * 1000)//连接超时时间 单位ms
                .retryPolicy(retryPolicy)//重试策略
                .namespace("chw")//名称空间
                .build();
        //开启连接
        client.start();

    }

    /**
     * NodeCache:给指定一个节点注册监听器
     */
    @Test
    public void listener() throws Exception {
        //1、创建NodeCache对象
        final NodeCache nodeCache = new NodeCache(client, "/节点一");
        //2、注册监听
        nodeCache.getListenable().addListener(() -> {
            System.out.println("节点变化了...");
            //获取修改节点后的数据
            byte[] data = nodeCache.getCurrentData().getData();
            System.out.println(new String(data));
        });
        //3、开启监听,如果设置为true,则开启监听时,加载缓存数据
        nodeCache.start(true);
        while (true) {

        }
    }

/**
     * 监控某一个节点ZNode的所有子节点们
     */
    @Test
    public void testPathChildrenCache() throws Exception {
        //1、创建NodeCache对象
        final PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/app2", true);
        //2、绑定监听器
        pathChildrenCache.getListenable().addListener((curatorFramework, pathChildrenCacheEvent) -> {
            System.out.println("子节点变化了...");
            //事件对象
            System.out.println(pathChildrenCacheEvent);
            //监听子节点的数据变更,并且拿到变更后的数据
            //1、获取类型
            PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
            //2、判断类型是否是update
            if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
                System.out.println("数据变更了...");
                byte[] data = pathChildrenCacheEvent.getData().getData();
                System.out.println(new String(data));
            }
        });
        //3、开启监听
        pathChildrenCache.start();
        while (true) {
        }
    }

 /**
     * TreeCache监控某个节点自己和其所有的子节点们,类似于PathChildrenCache和NodeCache的组合
     */
    @Test
    public void testTreeCache() throws Exception {
        //1、创建TreeCache对象
        final TreeCache treeCache = new TreeCache(client, "/节点一");
        //2、注册监听
        treeCache.getListenable().addListener((curatorFramework, treeCacheEvent) -> {
            System.out.println("节点或其子节点发生变化了");
            System.out.println(treeCacheEvent);
        });
        //3、开启监听
        treeCache.start();
        while (true) {
        }
    }


}

Zookeeper 实现分布锁

Zookeeper分布式锁的原理
核心思想:当请求需要获取锁时,则创建节点,使用完锁,则删除该节点。

节点能不能持久化,如果持久化节点,一旦获取锁的节点宕机了,会导致锁就不会被释放,节点不会被删除,如果节点不被删除,则其它节点就获取不到锁了,就会处于一直等待的状态;所以必须是临时的,即使宕机,临时节点也会在会话结束之后自动删除。

获取锁时,会在lock节点下创建临时顺序节点,顺序是因为要寻找最小节点,按照顺序去拿到锁,每次拿到的锁必须为节点序号最小的节点。 
所以当获取到lock下面的所有子节点之后,会进行判断自己创建的子节点序号是否最小,如果是,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。
如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
当比自己小的那个节点被删除,Watcher会收到相应通知,此时会再次判断自己创建的节点是否是lock子节点中序号最小的,如果是,则获取到了锁,如果不是,则重复以上步骤继续获取到比自己小的一个节点并注册监听。

具体实现:

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

模拟抢票功能

package eample;

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;

public class Ticket12306 implements Runnable {
    private int tickets = 10;//数据库的票数
    //锁
    private InterProcessMutex lock;

    public Ticket12306() {
        //重试策略
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("120.***.58:2181")
                .sessionTimeoutMs(60 * 1000)
                .connectionTimeoutMs(15 * 1000)
                .retryPolicy(retryPolicy)
                .build();
        //开启连接
        client.start();
        lock = new InterProcessMutex(client, "/lock");
    }

    @Override
    public void run() {
        while (true) {
            //获取锁
            try {
                lock.acquire();
                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();
                }
            }
        }
    }

    public static void main(String[] args) {
        Ticket12306 ticket12306 = new Ticket12306();
        //创建客户端
        Thread t1 = new Thread(ticket12306, "携程");
        Thread t2 = new Thread(ticket12306, "飞猪");
        t1.start();
        t2.start();
    }
}

Zab协议

Zab协议是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议 ,是Zookeeper保证数据一致性的核心算法。

Zab借鉴了Paxos算法,但又不像Paxos那样,是一种通用的分布式一致性算法。它是特别为Zookeeper设计的支持崩溃恢复的原子广播协议。

Zookeeper的核心是原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式和广播模式。
Zab 协议包括两种基本的模式:崩溃恢复 和 消息广播

协议过程
当整个集群启动过程中,或者当 Leader 服务器出现网络中弄断、崩溃退出或重启等异常时,Zab协议就会 进入崩溃恢复模式,选举产生新的Leader。当选举产生了新的 Leader,同时集群中有过半的机器与该 Leader 服务器完成了状态同步(即数据同步)之后,Zab协议就会退出崩溃恢复模式,进入消息广播模式。这时,如果有一台遵守Zab协议的服务器加入集群,因为此时集群中已经存在一个Leader服务器在广播消息,那么新加入的服务器自动进入恢复模式:找到Leader服务器,并且完成数据同步。同步完成后,作为新的Follower一起参与到消息广播流程中。

为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,前32位是epoch用来标识 leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。后32位用于递增计数。


Zab协议的核心:定义了事务请求的处理方式

  • 所有的事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被叫做 Leader服务器。其他剩余的服务器则是 Follower服务器。
  • Leader服务器 负责将一个客户端事务请求,转换成一个事务Proposal,并将该Proposal 分发给集群中所有的 Follower 服务器,也就是向所有 Follower 节点发送数据广播请求(或数据复制)
  • 分发之后Leader服务器需要等待所有Follower服务器的反馈(Ack请求),在Zab协议中,只要超过半数的Follower服务器进行了正确的反馈后(也就是收到半数以上的Follower的Ack请求),那么 Leader 就会再次向所有的 Follower服务器发送 Commit 消息,要求其将上一个事务proposal 进行提交。

你可能感兴趣的:(【JAVA】,java,java-zookeeper,zookeeper,zookeeper浅谈)