使用ZK进行开发

一、使用Zookpeer原生API

命名空间

Chroot特性允许每个客户端设置一个命名空间,如果一个Zookpeer客户端设置了Chroot,那么该客户端对服务器的任何操作,都将被限定在自己的命名空间下。
如果我们希望为应用分配/apps/X下的所有子节点,那么该应用可以将所有Zookpeer客户端的Chroot设置为/apps/X。一旦设置了Chroot后,那么对于这个客户端来说,所有的节点路径都以/apps/X为根节点。
客户端可以在connectString中以添加后缀的方式来设置,如:

192.168.56.101:2181,192.168.56.101:2182,192.168.56.101:2183/apps/X

负载均衡策略

StaticHostProvider是Zookpeer默认的一种非常简单的负载均衡策略,它的表现形式其实类似“Random Robin”策略。StaticHostProvider会从客户端输入的构成服务器地址,然后通过其next方法从serverAddreess中获取一个服务器地址时,会先将服务器地址打散然后拼装成一个环形循环列表。
如原始地址访问字符为:“host1,host2,host3,host4,host5”经过打散重新拼装后会构成环形列表:
在这里插入图片描述
初始化的时候currentIndex和lastIndex都是-1。每次尝试获取一个服务器地址的时候,都会将currentIndex向前移动一位,如果发现游标移动超过了整个地址列表的长度,那么就重置0,回到开始的位置重新开始。对于那些服务器列表提供的比较少的场景,StaticHostProvider如果发现当前游标位置和上次使用过的地址一样,即当currentIndex和lastIndex相同时,就进行spinDelay毫秒时间的等待。

// 初始化时currentIndex和lastIndex都为-1
// lastIndex表示当前正在使用的服务器地址位置
private int lastIndex = -1;

// currentIndex表示环形队列中当前遍历到的那个元素位置
private int currentIndex = -1;

private void init(Collection<InetSocketAddress> serverAddresses) {
    if (serverAddresses.isEmpty()) {
        throw new IllegalArgumentException(
            "A HostProvider may not be empty!");
    }

    this.serverAddresses.addAll(serverAddresses);
    // 打散集合
    Collections.shuffle(this.serverAddresses);
}

// 获取服务器地址函数
public InetSocketAddress next(long spinDelay) {
    // currentIndex向前移动一位并与服务器数量做“%”操作
    currentIndex = ++currentIndex % serverAddresses.size();
    // 如果currentIndex和上次访问的服务器相同,则休眠spinDelay毫秒
    if (currentIndex == lastIndex && spinDelay > 0) {
        try {
            Thread.sleep(spinDelay);
        } catch (InterruptedException e) {
            LOG.warn("Unexpected exception", e);
        }
    } else if (lastIndex == -1) {
        // We don't want to sleep on the first ever connect attempt.
        lastIndex = 0;
    }

    // 根据currentIndex获取地址
    InetSocketAddress curAddr = serverAddresses.get(currentIndex);
    try {
        // 把地址解析成字符串
        String curHostString = getHostString(curAddr);
        // 根据字符解析所有网络地址
        List<InetAddress> resolvedAddresses = new ArrayList<InetAddress>(Arrays.asList(this.resolver.getAllByName(curHostString)));
        // 如果List为空,代表没有可以解析到的其他主机
        if (resolvedAddresses.isEmpty()) {
            return curAddr;
        }
        // 如果List不为空,把解析到的集合再次打散
        Collections.shuffle(resolvedAddresses);
        // 返回打散集合的第一条数据
        return new InetSocketAddress(resolvedAddresses.get(0), curAddr.getPort());
    } catch (UnknownHostException e) {
        return curAddr;
    }
}

// 获取到服务器地址后,连接服务器时调用
public void onConnected(){
    // 让lastIndex等于currentIndex,相当于lastIndex移动到currentIndex的位置来表示最后一次访问的服务器
    lastIndex = currentIndex;
}

在使用ZooKeeper的Java客户端时,经常需要处理几个问题:

  • Watcher反复注册
  • Session超时重连
  • 异常处理

要解决上述的几个问题,可以自己解决,也可以采用第三方的Java客户端来完成。

二、使用ZkClient

创建会话

同步方法,自动重连

public ZkClient(final String zkServers, final int sessionTimeout, 
       final int connectionTimeout, final ZkSerializer zkSerializer, 
       final long operationRetryTimeout) 

创建节点

同步,可以递归创建

public String create(String path,Object data,final List<ACL> acl,CreateMode mode)
public void createPersistent(String path,boolean createParents,List<ACL> acl)
public void createPersistent(String path, Object data, List<ACL> acl)
public String createPersistentSequential(String path,Object data,List<ACL> acl)
public void createEphemeral(String path, Object data, List<ACL> acl)
public String createEphemeralSequential(String path,Object data,List<ACL> acl)

删除节点

同步,可以提供递删除

public boolean delete(String path,int version)
public boolean deleteRecursive(String path)

获取节点

同步,避免不存在异常

public List<String> getChildren(String path)
public <T> T readData(String path, boolean returnNullIfPathNotExists)
public <T> T readData(String path, Stat stat)

更新节点

同步,实现CAS,状态返回

public void writeData(String path, Object datat, int expectedVersion)
public Stat writeDataReturnStat(String path,Object datat,int expectedVersion)

判断节点是否存在

public boolean exists(String path) 

事件监听

public List<String> subscribeChildChanges(String path, IZkChildListener listener)

事件通知

public void handleChildChange(String parentPath, List<String> currentChilds)

三、使用Curator

Curator是Netflix公司开源的一个Zookpeer客户端,与Zookpeer提供的原生客户端相比,Curator的抽象层次更高,简化了Zookpeer客户端的开发量。

  • 封装Zookpeer client与Zookpeer server之间的连接处理
  • 提供了一套Fluent风格的API
  • 提供Zookpeer各种应用场景(recipe,比如共享锁服务,集群领导选举机制)的抽象封装

Curator几个组成部分

  • Client:是Zookpeer客户端的一个替代品,提供了一些底层处理和相关的工具方法。
  • Framework:用来简化Zookpeer高级功能的使用,并增加了一些新的功能,比如管理到Zookpeer集群的连接,重试处理。
  • Recipes:实现了通用Zookpeer的recipe,该组件建立在Framework的基础上。
  • Utilities:各种Zookpeer的工具类。
  • Errors:异常处理,连接,恢复等。
  • Extensions:recipe扩展。

RetryPolicy连接策略

  • RetryOneTime:只重连一次
  • RetryNTime:指定重连的次数N
  • RetryUtilElapsed:指定最大重连超时时间和重连时间间隔,间歇性重连知道超时或者连接成功
  • ExponentialBackoffRetry:基于“backoff(退避)”方式重连,和RetryUtilElapsed的区别是重连的时间间隔是动态的
  • BoundedExponentialBackoffRetry:同ExponentialBackoffRetry,增加了最大重试次数的控制

Curator的API

创建会话

CuratorFrameworkFactory.newClient(String connectString, int sessionTimeoutMs, 
				int connectionTimeoutMs, RetryPolicy retryPolicy)

CuratorFrameworkFactory.builder().connectString("192.168.11.56:2180")  
		    .sessionTimeoutMs(30000).connectionTimeoutMs(30000)  
		    .canBeReadOnly(false)  
		    .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))
		    .build();

创建节点

client.create().creatingParentIfNeeded()
        .withMode(CreateMode.PERSISTENT)
        .withACL(aclList)
        .forPath(path, "hello, zk".getBytes());

删除节点

client.delete().guaranteed().deletingChildrenIfNeeded().withVersion(version).forPath(path)

获取节点

client.getData().storingStatIn(stat).forPath(path);

client.getChildren().forPath(path);

更新节点

client.setData().withVersion(version).forPath(path, data)

判断节点是否存在

client.checkExists().forPath(path)

设置权限

Build.authorization(String scheme, byte[] auth)
client.setACL().withVersion(version)
        .withACL(ZooDefs.Ids.CREATOR_ALL_ACL)
        .forPath(path);

监听器

Cache是curator中对事件监听的包装,对事件的监听可以近似看作是本地缓存视图和远程ZK视图的对比过程

  • NodeCache节点缓存用于处理节点本身的变化,回调接口NodeCacheListener
  • PathChildrenCache子节点缓存用于处理节点的子节点的变化,回调接口PathChildrenCacheListener
  • TreeCache/NodeCache和PathChildrenCache的结合体,回调接口TreeCacheListener

四、应用场景

配置中心

项目中用到的数据库信息一般都是写在配置文件中,如果需要修改配置信息,通常先要修改配置文件然后再进行部署。加入集群中有几百个节点上的App需要修改配置,再使用修改后部署的方式就会非常麻烦,这个时候就可以用到统一配置管理。
在这里插入图片描述
配置中心原理说明:

  • 配置项放于Zookeeper中
  • 对公共配置修改后发布到Zookpeer
  • 对ZK配置节点监听,配置一旦被修改,应用可实时监听到并获取

Master选举

对外提供7*24小时服务的系统,如采用的是Master+Slave集群,不能有单点故障。集群中由主机向外提供服务,备机监听主机状态,一旦主机宕机,备机必须迅速接管主机继续向外提供服务。在这个过程中,从备机选出一台作为主机的过程,就是Master选举。
在这里插入图片描述
经典的运用场景
经典的应用场景

  1. 单点故障(如Hadoop的高可用,HBase的HMaster)
  2. 较为耗时的计算任务可以交给一台机器去执行的

分布式锁

线程锁: 只在同一个JVM中有效果,根本上是依靠线程之间共享内存实现的。
分布式锁: 多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的主机需要访问共享资源,在访问这些资源的时候需要通过一些互斥手段防止彼此之间的干扰。
分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性。
在这里插入图片描述
在这里插入图片描述
第一张图的整个区域表示一个Zookpeer集群,LOCK是Zookpeer的一个持久节点,0000001、0000002、0000003、000000n是LOCK这个持久节点下面的临时顺序节点,它们分别代表来自Client_1、Client_2、Client_3、Client_n的客户端请求。
算法思路: 利用名称唯一性,加锁操作时,只需要所有的客户端一起创建/test/Lock节点,只有一个创建成功,成功者获得锁。解锁时,只需要删除该节点,其余客户端再次进入创建节点,知道所有客户端都获得锁。
特点: 这种方案的正确性和可靠性是Zookpeer机制保证的,实现简单。缺点是会产生惊群效应,假如许多客户端在等待一把锁,当锁释放的时候所有的客户端都被唤醒,仅仅有一个客户端得到锁。

优化算法

  1. 在获取分布式锁的时候在LOCK节点下创建临时顺序节点,释放锁的时候删除该临时节点。客户端调用createNode方法在LOCK下创建临时顺序节点。然后调用getChildren(“LOCK”)来获取LOCK下面的所有子节点,注意此时不用设置任何Watcher。
  2. 客户端获取到所有的子节点path之后,如果发现自己在之前创建的子节点序号最小,那么就认为该客户端获取到了锁。如果发现自己创建的节点并非是LOCK所有子节点中最小的,说明自己还没有获取到锁。
  3. 此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。
  4. 之后,如果这个被关注的节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是LOCK子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤。
    在这里插入图片描述

服务注册发现

每个服务向注册中心注册登记自己提供的服务,服务注册之后,注册中心会维护这份注册清单,服务提供者会周期性地向Server发送心跳以续约自己的信息。如:Eureka Server在一定时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服务节点(默认90秒);
服务发现
在微服务中,服务的调用不再通过指定的地址来实现,而是通过向服务名发起请求调用实现。
解决的问题

  1. 服务提供者和调用者间的解耦
  2. 使用服务名称而不是IP+Port端口号访问服务
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值