zookeeper 服务器动态上下线监听案例和分布式锁案例

本文介绍了如何使用Zookeeper进行服务器动态上下线监听,包括创建服务器节点、客户端监听和响应变化。同时,文章详细讲解了分布式锁的概念,并通过原生Zookeeper API和Curator框架分别实现分布式锁,展示了锁的获取、释放以及并发控制的过程。此外,还对比了原生API和Curator在开发中的优缺点。
摘要由CSDN通过智能技术生成

一、服务器动态上下线监听案例

1.1 需求

        在某分布式系统中,存在多台主节点且可以动态上下线,需要让任意一台客户端都能实时感知到主节点服务器的上下线。

1.2 需求分析

1.3 具体实现

        1、现在集群上创建 /servers 节点

[zk: mylinux01:2181(CONNECTED) 10] create /servers "servers"
Created /servers
[zk: mylinux01:2181(CONNECTED) 11] ls /
[servers, zookeeper]

        2、idea 中创建包名:com.zkCase1

        3、编写服务端向 zookeeper 注册的代码,如下:

package com.zkCase1;

import org.apache.zookeeper.*;

import java.io.IOException;

public class DistributeServer {

    // 注意:逗号前后不能有空格
    private static String connectString = "192.168.229.165:2181,192.168.229.169:2181,192.168.229.168:2181";
    private static int sessionTimeout = 30000;
    private ZooKeeper zkClient ;
    private String parentNode = "/servers";

    public static void main(String[] args) throws Exception {
        // 1 获取 zk 连接
        DistributeServer server = new DistributeServer();
        server.getConnect();
        // 2 利用 zk 连接注册服务器信息
        server.registerServer(args[0]);
        // 3 启动业务功能
        server.business(args[0]);
    }
    // 创建到 zk 的客户端连接
    public void getConnect() throws IOException {
        zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {

            }
        });
    }
    // 注册服务器,相当于创建一个带序号的临时节点
    public void registerServer(String hostname) throws Exception{
        String create = zkClient.create(parentNode +"/"+ hostname, hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(hostname +" is online "+ create);
    }
    // 业务功能
    public void business(String hostname) throws Exception{
        System.out.println(hostname + " is working ...");
        Thread.sleep(Long.MAX_VALUE);
    }
}

        4、客户端代码如下:

public class DistributeClient {
    // 注意:逗号前后不能有空格
    private static String connectString = "192.168.229.165:2181,192.168.229.169:2181,192.168.229.168:2181";
    private static int sessionTimeout = 30000;
    private ZooKeeper zkClient ;
    private String parentNode = "/servers";

    // 创建到 zk 的客户端连接
    public void getConnect() throws IOException {
        zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 再次启动监听
                try {
                    getServerList();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
    // 获取服务器列表信息
    public void getServerList() throws Exception {
        // 1 获取服务器子节点信息,并且对父节点进行监听
        List<String> children = zkClient.getChildren(parentNode, true);
        // 2 存储服务器信息列表
        ArrayList<String> servers = new ArrayList<>();
        // 3 遍历所有节点,获取节点中的主机名称信息
        for (String child : children) {
            byte[] data = zkClient.getData(parentNode + "/" + child, false, null);
            servers.add(new String(data));
        }
        // 4 打印服务器列表信息
        System.out.println(servers);
    }
    // 业务功能
    public void business() throws Exception{
        System.out.println("client is working ...");
        Thread.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws Exception {
        // 1 获取 zk 连接
        DistributeClient client = new DistributeClient();
        client.getConnect();
        // 2 获取 servers 的子节点信息,从中获取服务器信息列表
        client.getServerList();
        // 3 业务进程启动
        client.business();
    }
}

1.4 测试

1.4.1 在 linux 命令行增删服务器

        1、启动 DistributeClient 客户端代码

        2、mylinux01 上的 zookeeper 客户端 /servers 目录下创建带序号的临时节点

[zk: mylinux01:2181(CONNECTED) 13] create -e -s /servers/mylinux01 "test"
Created /servers/mylinux010000000000
[zk: mylinux01:2181(CONNECTED) 14] create -e -s /servers/mylinux02 "test2"
Created /servers/mylinux020000000001

        3、观察 idea 控制台的变化,如下图:

        4、执行删除操作

[zk: mylinux01:2181(CONNECTED) 17] delete /servers/mylinux010000000000

        5、观察 idea 控制台的变化,如下图:

1.4.2 在 idea 上增删服务器

        1、启动 DistributeClient 客户端代码,如果启动过了就不用启动了

        2、启动 DistributeServer 服务端代码,如下

        在弹出的窗口中(Program arguments)输入想启动的主机,例如,mylinux01

        回到 DistributeServer main 方法,点击运行

        观察 DistributeServer 控制台,提示 mylinux01 is working 

        观察 DistributeClient 控制台,提示 mylinux01 已经上线

二、分布式锁案例

2.1 分布式锁概念

        什么是分布式锁呢?比如说 "进程1" 在使用该资源的时候,会先去获得锁,"进程1" 获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程1" 用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。

2.2 需求分析

        1、此时有若干个客户端分别要对 zookeeper 集群进行写入操作,zookeeper 集群接收到请求后,会分别在 /locks 节点下创建一个临时顺序节点。

        2、 zookeeper 规定序号最小的节点有第一优先级获取到锁,然后操作锁里面对应的资源。以上图为例,/seq-000000000 节点会先获取到锁,等它操作完了,这个节点就可以释放掉,下一个节点就可以获取到锁。

        3、判断自己是不是当前节点下最小的节点,如果是则获取锁,如果不是则对前一个节点进行监听。

        4、获取到锁,处理完业务后,会删除节点释放锁,然后下面的节点将收到通知,重复第三步的操作。

2.3 原生 zookeeper 实现分布式锁

2.3.1 分布式锁实现

package com.lock;
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;

public class DistributedLock {
    // zookeeper server 列表
    private String connectString = "192.168.229.165:2181,192.168.229.169:2181,192.168.229.168:2181";
    // 超时时间
    private int sessionTimeout = 30000;
    private ZooKeeper zk;
    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 {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
                if (event.getState() ==
                        Event.KeeperState.SyncConnected) {
                    connectLatch.countDown();
                }
                // 发生了 waitPath 的删除事件
                if (event.getType() ==
                        Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
                    waitLatch.countDown();
                }
            }
        });
        // 等待连接建立
        connectLatch.await();
        //获取根节点状态
        Stat stat = zk.exists("/" + rootNode, false);
        //如果根节点不存在,则创建根节点,根节点类型为永久节点
        if (stat == null) {
            System.out.println("根节点不存在");
            zk.create("/" + rootNode, new byte[0],ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    // 加锁方法
    public void zkLock() {
        try {
            //在根节点下创建临时顺序节点,返回值为创建的节点路径
            currentNode = zk.create("/" + rootNode + "/" + subNode,
                    null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            // wait 一小会, 让结果更清晰一些
            Thread.sleep(10);
            // 注意, 没有必要监听"/locks"的子节点的变化情况
            List<String> childrenNodes = zk.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 获得锁
                    return;
                } else {
                    // 获得排名比 currentNode 前 1 位的节点
                    this.waitPath = "/" + rootNode + "/" +
                            childrenNodes.get(index - 1);
                    // 在 waitPath 上注册监听器, 当 waitPath 被删除时,zookeeper 会回调监听器的 process 方法
                    zk.getData(waitPath, true, new Stat());
                    //进入等待锁状态
                    waitLatch.await();
                    return;
                }
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

2.3.2 分布式锁测试

package com.lock;
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(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                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(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock2.zkLock();
                    System.out.println("线程 2 获取锁");
                    Thread.sleep(5 * 1000);
                    lock2.zkUnlock();
                    System.out.println("线程 2 释放锁");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

2.4 Curator 框架实现分布式锁

2.4.1 原生 api 开发存在的问题

        1、会话连接是异步的,需要自己去处理。比如需要使用 CountDownLatch

        2、Watch 需要重复注册,不然就不能生效

        3、开发的复杂性还是比较高的

        4、不支持多节点删除和创建。需要自己去递归

2.4.2 Curator 简介

        Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。详情请查看官方文档:https://curator.apache.org/index.html

2.4.3 Curator 案例实战

        1、添加 maven 依赖

<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>

        2、代码实现

package com.lock2;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class CuratorLockTest {
    private String rootNode = "/locks";
    // zookeeper server 列表
    private String connectString ="192.168.229.165:2181,192.168.229.169:2181,192.168.229.168:2181";
    // connection 超时时间
    private int connectionTimeout = 30000;
    // session 超时时间
    private int sessionTimeout = 30000;
    public static void main(String[] args) {
        new CuratorLockTest().test();
    }
    // 测试
    private void test() {
        // 创建分布式锁 1
        final InterProcessLock lock1 = new InterProcessMutex(getCuratorFramework(), rootNode);
        // 创建分布式锁 2
        final InterProcessLock lock2 = new InterProcessMutex(getCuratorFramework(), rootNode);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock1.acquire();
                    System.out.println("线程 1 获取锁");
                    // 测试锁重入
                    lock1.acquire();
                    System.out.println("线程 1 再次获取锁");
                    Thread.sleep(5 * 1000);
                    lock1.release();
                    System.out.println("线程 1 释放锁");
                    lock1.release();
                    System.out.println("线程 1 再次释放锁");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock2.acquire();
                    System.out.println("线程 2 获取锁");
                    // 测试锁重入
                    lock2.acquire();
                    System.out.println("线程 2 再次获取锁");
                    Thread.sleep(5 * 1000);
                    lock2.release();
                    System.out.println("线程 2 释放锁");
                    lock2.release();
                    System.out.println("线程 2 再次释放锁");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    // 分布式锁初始化
    public CuratorFramework getCuratorFramework (){
        //重试策略,初试时间 3 秒,重试 3 次
        RetryPolicy policy = new ExponentialBackoffRetry(3000, 3);
        //通过工厂创建 Curator
        CuratorFramework client =
                CuratorFrameworkFactory.builder()
                        .connectString(connectString)
                        .connectionTimeoutMs(connectionTimeout)
                        .sessionTimeoutMs(sessionTimeout)
                        .retryPolicy(policy).build();
        //开启连接
        client.start();
        System.out.println("zookeeper 初始化完成...");
        return client;
    }
}

        3、观察控制台的变化,如下:

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值