Zookeeper应用 配置管理和分布式锁

由于zk的一些特性:速度快、高可用、临时节点、序列节点、watcher等
所以可用zk来实现分布式的配置管理(如注册中心) 或者 分布式锁
这里简单用java + zk来实现

准备

idea新建springboot项目,主要是POM要引入Zookeeper的包,注意包的版本必须和zk服务器的版本一致!
在这里插入图片描述

分布式配置

在这里插入图片描述
如图,分布式节点连接zk集群,watch其中的节点数据(配置),则在节点数据被修改时,会向每个session发送callback,因此分布式节点能够实时更新到配置的信息
代码
ZKUtil用于获取zk客户端

import java.util.concurrent.CountDownLatch;

/**
 * @author MasterYee
 * @Description:
 * @date: 2020/4/24
 */
public class ZKUtils {

    public static ZooKeeper getZK() {
        ZooKeeper zk;
        try {
            CountDownLatch cd = new CountDownLatch(1);
            DefaultWatcher watcher = new DefaultWatcher();
            // connectString是所有集群的地址,如果在后面加了/nodename,就表示这个连接以当前节点为根目录,否则就是/为根目录
            // timeout是session断开后维持多久 (session挂则临时节点挂)
            // watcher监控了当前整个session
            zk = new ZooKeeper("192.168.153.110:2181,192.168.153.111:2181,192.168.153.112:2181,192.168.153.113:2181/locktest",
                    3000, watcher);
            watcher.setCd(cd);
            cd.await(); // 阻塞住,知道连接成功后释放门栓

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return zk;
    }
}

DefaultWatcher 创建连接时的watch

package com.example.zkdemo;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;

import java.util.concurrent.CountDownLatch;

/**
 * @author MasterYee
 * @Description:
 * @date: 2020/4/24
 */
public class DefaultWatcher implements Watcher {

    private CountDownLatch cd;


    @Override
    public void process(WatchedEvent event) {
        System.out.println("---------------------watching event----------");
        System.out.println("event : " + event.toString());
        // 由于是异步执行的,当连接成功后,触发事件异步调用process,但此时程序已经跑完了!所以用一个门栓来控制
        Event.KeeperState state = event.getState();
        switch (state) {
            case Unknown:
                break;
            case Disconnected:
                break;
            case NoSyncConnected:
                break;
            case SyncConnected:
                cd.countDown(); // 门栓-- 放行
                System.out.println("connected....");
                break;
            case AuthFailed:
                break;
            case ConnectedReadOnly:
                break;
            case SaslAuthenticated:
                break;
            case Expired:
                break;
            case Closed:
                break;
        }

    }

    public CountDownLatch getCd() {
        return cd;
    }

    public void setCd(CountDownLatch cd) {
        this.cd = cd;
    }
}

注意上面用了门栓,这是因为程序运行太快了,运行完主线程就退出了,建立了连接之后的callback都没有调用到程序就跑完了
Test类

package com.example.zkdemo;

import org.apache.zookeeper.ZooKeeper;

import java.util.concurrent.CountDownLatch;

/**
 * @author MasterYee
 * @Description: zookeeper 做配置管理
 * @date: 2020/4/24
 */
public class TestConfig {
    static ZooKeeper zk;


    public static void main(String[] args) {

        zk = ZKUtils.getZK();

        getConf();



        while (true);
    }


    /**
     * 先判断是否存在 会返回一个callback
     * 在callback里异步判断是否存在 存在则get
     * get同样会返回一个callback 在这个callback里将data赋值
     */
    public static void getConf() {
        WatchCallback watchCallback = new WatchCallback();
        watchCallback.setZk(zk);
        CountDownLatch cd = new CountDownLatch(1);
        watchCallback.setCd(cd);
        watchCallback.setPath("/AppConf");

        watchCallback.await();

    }
}

这里有一个WatchCallback,这是因为reactor模型异步调用太深了,所以用一个WatchCallback来封装,它既是watcher又是datacallback又是statcallback
WatchCallback

package com.example.zkdemo;

import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.util.concurrent.CountDownLatch;

/**
 * @author MasterYee
 * @Description:
 * @date: 2020/4/24
 */
public class WatchCallback implements Watcher, AsyncCallback.StatCallback, AsyncCallback.DataCallback {

    private ZooKeeper zk;

    private CountDownLatch cd;

    private String path;

    private String retData;


    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public CountDownLatch getCd() {
        return cd;
    }

    public void setCd(CountDownLatch cd) {
        this.cd = cd;
    }

    public ZooKeeper getZk() {
        return zk;
    }

    public void setZk(ZooKeeper zk) {
        this.zk = zk;
    }

    // DataCallback的回调 (get时)
    @Override
    public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
        if (data != null) {
            // get的回调有数据
            retData = new String (data);
        }
    }

    // StatCallback的回调 (exits时)
    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {
        if(stat != null) {
            // 有数据时 get数据
            zk.getData(path,this,this,"ABC");

        }else {
            System.out.println("没有数据: " + path);
        }
    }

    // 监听到watch事件
    @Override
    public void process(WatchedEvent event) {
        Event.EventType type = event.getType();
        switch (type) {
            case None:
                break;
            case NodeCreated:
                System.out.println("node created");
                // 监听到node添加时 获取配置
                zk.getData(path,this,this,"ABC");
                break;
            case NodeDeleted:
                // 监听到node删除时
                System.out.println("node deleted!");
                this.retData = null;
                break;
            case NodeDataChanged:
                System.out.println("node changed");
                // 监听到node改变时 重新获取配置
                zk.getData(path,this,this,"ABC");
                break;
            case NodeChildrenChanged:
                break;
            case DataWatchRemoved:
                break;
            case ChildWatchRemoved:
                break;
            case PersistentWatchRemoved:
                break;
        }
    }


    /**
     * 阻塞地去获取配置
     * 先判断是否存在 会返回一个callback
     * 在callback里异步判断是否存在 存在则get
     * get同样会返回一个callback 在这个callback里将data赋值
     */
    public void await() {
        zk.exists(path,this,this,"ABC");
        try {


            while (true) {
                Thread.sleep(1000);
                System.out.println("获得配置: " + retData);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String getRetData() {
        return retData;
    }

    public void setRetData(String retData) {
        this.retData = retData;
    }
}

create节点或者修改或者删除节点后,客户端都会收到回调,收到信息。
在这里插入图片描述在这里插入图片描述

分布式锁

设计思路:

首先肯定是设置临时节点,这样在程序出错session断开时不会产生死锁
有且仅有一个人能拿到锁,且在运行完后需要释放锁
可以是多个线程exists锁,如果存在则阻塞等待,不存在则创建并获得锁,当一个线程释放后,争抢锁
如何监听到锁释放?1:程序主动轮询,轮询去访问锁是否释放 2:watch+callback,zk释放锁时回调程序。这里肯定是2,因为1会有延迟+轮询压力
所以思路可以是临时节点+watch,但一个锁释放后,其它所有节点都将收到callback,如果节点过多,这样也会造成通信的压力
最终思路 sequence+watch+临时节点,每个节点获取锁时先创建序列节点,然后watch前一个序列节点,通过getchildren获取所有序列节点并排序,如果当前自己排在第一位则获取锁;释放锁时删除这个序列节点,则下一个节点callback,下一个节点获取到锁。
TestLock

package com.example.zkdemo.lock;

import com.example.zkdemo.ZKUtils;
import org.apache.zookeeper.ZooKeeper;

/**
 * @author MasterYee
 * @Description:
 * @date: 2020/4/24
 */
public class TestLock {
    public static void main(String[] args) {


        // 起10个线程 每个线程尝试去拿锁
        for (int i = 0; i < 10 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        ZKLock lock = new ZKLock();
                        ZooKeeper zk = ZKUtils.getZK();
                        lock.setZk(zk);
                        lock.setThreadName(Thread.currentThread().getName());

                        lock.tryLock();
                        System.out.println("线程获得锁: " + Thread.currentThread().getName());
                        Thread.sleep(100);
                        lock.unLock();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }).start();
        }

    }
}



ZKLock

package com.example.zkdemo.lock;

import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * @author MasterYee
 * @Description:
 * @date: 2020/4/24
 */
public class ZKLock implements AsyncCallback.Create2Callback, AsyncCallback.Children2Callback , Watcher, AsyncCallback.StatCallback {
    private CountDownLatch cd = new CountDownLatch(1);
    private ZooKeeper zk;
    private String threadName;
    private String pathName;

    public void tryLock () {
        try {
            System.out.println("threadName尝试获取锁.. " + threadName);
            // 临时+序列节点
            zk.create("/lock",threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL,this,"ABC");
            cd.await();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }

    public void unLock () {
        try {
            zk.delete(pathName,-1);
            System.out.println(threadName + " over work....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    // 创建完的回调
    // 返回的name是创建的序列临时节点的名称
    @Override
    public void processResult(int rc, String path, Object ctx, String name, Stat stat) {
        // 创建成功后获取所有的子节点
        System.out.println("线程create node : " + threadName + "  " + name);
        pathName = name;
        zk.getChildren("/",false,this,"ABC");
    }



    // getChildre时的回调
    @Override
    public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
        // 对children排序 找到序列最小的
        Collections.sort(children);

        int i = children.indexOf(pathName.substring(1));

        if(i == 0) {
            // 是第一个线程
            cd.countDown();
        } else {
            // 不是第一个节点 就watch上一个
            zk.exists("/" + children.get(i-1),this,this,"ABC");

        }
    }


    // watch的监听方法
    @Override
    public void process(WatchedEvent event) {
        Event.EventType type = event.getType();
        switch (type) {
            case None:
                break;
            case NodeCreated:
                break;
            case NodeDeleted:
                // 上一个节点被删除了,这个节点收到了回调事件
                // 尝试拿锁
                zk.getChildren("/",false,this,"ABC");
                break;
            case NodeDataChanged:
                break;
            case NodeChildrenChanged:
                break;
            case DataWatchRemoved:
                break;
            case ChildWatchRemoved:
                break;
            case PersistentWatchRemoved:
                break;
        }
    }

    // statCallback的回调
    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {

    }


    public String getPathName() {
        return pathName;
    }

    public void setPathName(String pathName) {
        this.pathName = pathName;
    }

    public ZooKeeper getZk() {
        return zk;
    }

    public void setZk(ZooKeeper zk) {
        this.zk = zk;
    }

    public void setCd(CountDownLatch cd) {
        this.cd = cd;
    }

    public CountDownLatch getCd() {
        return cd;
    }

    public String getThreadName() {
        return threadName;
    }

    public void setThreadName(String threadName) {
        this.threadName = threadName;
    }
}

在这里插入图片描述
可以看到,每个线程拿锁时,会往zk里设置一个临时序列节点(以lock开头、值为线程名(可不要)),创建完节点后,当前线程的门栓拴住、阻塞,创建的回调方法去获取所有的children,获取所有孩子的回调方法中又对孩子进行了排序、当当前节点是最小的节点值时门栓放开,线程继续,最后释放锁(删除节点);当当前节点不是最小节点时,watch前一个节点,直到上一个节点删除,会重新获取一次所有孩子(也就是重新尝试拿锁)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值