基于zookeeper集群实现分布式锁的工程实践

|▌冷眼眸甩不掉的悲伤 提交于 2020-02-18 04:35:35

上一篇实现了基于zookeeper集群实现的分布式配置中心的工程样例,并进行了总结,本篇进行基于zookeeper集群实现的分布式锁工程案例,当然也借鉴和参考了其他博客,这里先给出引用:https://blog.csdn.net/hongtaolong/article/details/88898875

一、定义上下文锁的抽象对象

package com.coderman.zookeeper.clusterdemo.distributelockdemo;

/**
 * @description:
 * @author: Fanchunshuai
 * @time: 2020/2/15 16:04
 * 定义一个bean对象表示需要用锁的场景的上下文数据模型
 * 分布式锁可能需要定义持久化节点和临时节点,以及临时顺序节点
 * 这里假定持久化节点为:/group/appName
 * 临时节点为:/group/appName/lockPath
 * 临时顺序节点:/group/appName/lockPath0000000000001
 */
public class LockConfigBean {
    /**
     * 集群节点目录一级分组
     */
    private String group;
    /**
     * 集群中的应用名称
     */
    private String appName;
    /**
     * 锁的目录名
     */
    private String lockPath;

    /**
     * 获取锁的重试间隔时间
     */
    private long waitTime;

    public long getWaitTime() {
        return waitTime;
    }

    public void setWaitTime(long waitTime) {
        this.waitTime = waitTime;
    }

    public String getGroup() {
        return group;
    }

    public void setGroup(String group) {
        this.group = group;
    }

    public String getAppName() {
        return appName;
    }

    public void setAppName(String appName) {
        this.appName = appName;
    }

    public String getLockPath() {
        return lockPath;
    }

    public void setLockPath(String lockPath) {
        this.lockPath = lockPath;
    }

    @Override
    public String toString() {
        return "LockConfigBean{" +
                "group='" + group + '\'' +
                ", appName='" + appName + '\'' +
                ", lockPath='" + lockPath + '\'' +
                '}';
    }
}

二、定义锁的抽象操作接口

package com.coderman.zookeeper.clusterdemo.distributelockdemo;

/**
 * @description:
 * @author: Fanchunshuai
 * @time: 2020/2/15 16:02
 * zookeeper实现分布式锁,定义一个接口
 * 定义锁需要的基本操作
 */
public interface ZKLock {
    /**
     * 尝试获取锁
     * @param lockConfigBean
     * @return
     * @throws Exception
     */
    boolean tryLock(LockConfigBean lockConfigBean) throws Exception;

    /**
     * 获取锁,并执行业务代码之后需要显示调用该方法释放锁
     * 释放锁
     * @param lockConfigBean
     * @return
     * @throws Exception
     */
    boolean releaseLock(LockConfigBean lockConfigBean) throws Exception;

    /**
     * 直接获取锁,通过递归或者循环获取锁屏蔽底层细节
     * @param lockConfigBean
     * @return
     * @throws Exception
     */
    boolean getLock(LockConfigBean lockConfigBean) throws Exception;
    /**
     * 当获取锁的时候并发比较高无法立刻获取锁那就需要监听等待其他线程或者服务节点释放锁
     *
     * @param lockConfigBean
     * @throws Exception
     */
    void waitLock(LockConfigBean lockConfigBean) throws Exception;
}

三、实现简单版本的分布式锁服务

package com.coderman.zookeeper.clusterdemo.distributelockdemo;

import com.coderman.zookeeper.clusterdemo.ZKClientUtils;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;

import java.util.concurrent.CountDownLatch;

/**
 * @description:
 * @author: Fanchunshuai
 * @time: 2020/2/15 16:14
 * 简单版本的分布式锁实现
 */
public class SimpleLockImpl implements ZKLock {
    private  ZKClientUtils zkClientUtils = new ZKClientUtils();
    private CountDownLatch countDownLatch;


    @Override
    public boolean tryLock(LockConfigBean lockConfigBean) throws Exception {
        //构造临时节点
        String ePath = getEphemeral(lockConfigBean);
        ZkClient zkClient = zkClientUtils.getZkClient();
        if(zkClient == null){
            return false;
        }
        try {
            //创建临时节点
            zkClient.createEphemeral(ePath);
        }catch (Exception e){

            return false;
        }
        return true;
    }

    @Override
    public boolean releaseLock(LockConfigBean lockConfigBean) throws Exception {
        String ePath = getEphemeral(lockConfigBean);
        ZkClient zkClient = zkClientUtils.getZkClient();
        if(zkClient != null){
            //删除临时节点
            zkClient.delete(ePath);
            zkClient.close();
            System.out.println(Thread.currentThread().getName()+"释放锁成功");
        }
        return true;
    }

    @Override
    public boolean getLock(LockConfigBean lockConfigBean) throws Exception {
       /* if(!tryLock(lockConfigBean)){
            System.out.println(Thread.currentThread().getName()+"获取锁失败.");
            //等待一定时间
            //或者等待一定次数去递归获取锁,但是zookeeper有另外的监听机制可以代替等待频率,但是是阻塞等待
            //因此我们需要一个waitLock的api去监听释放锁,注释代码是第一版内容
            Thread.sleep(lockConfigBean.getWaitTime());
            //再次获取
            return getLock(lockConfigBean);
        }else {
            return true;
        }*/

        if(tryLock(lockConfigBean)){
            System.out.println(Thread.currentThread().getName()+"获取锁成功.");
            return true;
        }else {
            System.out.println(Thread.currentThread().getName()+"获取锁失败.");

            //获取锁失败就等待
            waitLock(lockConfigBean);
            //递归重新尝试获取锁
            return getLock(lockConfigBean);
        }
    }

    @Override
    public void waitLock(LockConfigBean lockConfigBean) throws Exception {
        String ePath = getEphemeral(lockConfigBean);

        IZkDataListener zkDataListener = new IZkDataListener() {
            //节点变化回调
            @Override
            public void handleDataChange(String s, Object o) throws Exception {
                /*if(countDownLatch != null){
                    countDownLatch.countDown();
                }*/
            }

            @Override
            public void handleDataDeleted(String s) throws Exception {
                if(countDownLatch != null){
                    countDownLatch.countDown();
                }
            }
        };
        ZkClient zkClient = zkClientUtils.getZkClient();
        //注册监听器
        zkClient.subscribeDataChanges(ePath,zkDataListener);
        if(zkClient.exists(ePath)){
            countDownLatch = new CountDownLatch(1);
            try {
                System.out.println(Thread.currentThread().getName()+"等待获取锁....");
                countDownLatch.await();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        //删除监听
        zkClient.unsubscribeDataChanges(ePath,zkDataListener);

    }

    /**
     * 获取临时节点
     * @param lockConfigBean
     * @return
     */
    private String getEphemeral(LockConfigBean  lockConfigBean){
       return  "/"+lockConfigBean.getGroup()+"-"+lockConfigBean.getAppName()+"-"+lockConfigBean.getLockPath();
    }
}

四、实现高性能版本的分布式锁服务

package com.coderman.zookeeper.clusterdemo.distributelockdemo;

import com.coderman.zookeeper.clusterdemo.ZKClientUtils;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;

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

/**
 * @description:
 * @author: Fanchunshuai
 * @time: 2020/2/16 10:27
 */
public class HighPerformanceLockImpl implements ZKLock {
    private ZKClientUtils zkClientUtils = new ZKClientUtils();

    //当前节点
    private String currentPath = "";
    //当前节点的上一个节点
    private String beforePath = "";
    private CountDownLatch countDownLatch;

    private  final ThreadLocal<String> threadLocal = new ThreadLocal();
    @Override
    public boolean tryLock(LockConfigBean lockConfigBean) throws Exception {
        //构造持久节点
        String ePath = getPersist(lockConfigBean);
        ZkClient zkClient = zkClientUtils.getZkClient();
        if(zkClient == null){
            return false;
        }
        try {
            //持久节点不存在 这里需要提前进行初始化避免再次跟zk集群进行交互
            if(!zkClient.exists(ePath)){
                //创建持久节点
                zkClient.createPersistent(ePath);
            }
            if(currentPath == null || "".equals(currentPath)){
                currentPath = zkClient.createEphemeralSequential(ePath+"/","lock");
            }
            //获取所有的临时节点,并排序
            List<String> childrens = zkClient.getChildren(ePath);
            Collections.sort(childrens);
            if(currentPath.equals(ePath+"/"+childrens.get(0))){
                return true;
            }else {
               //如果当前节点不是排名第一,则获取它前面的节点名称,
               //并赋值给beforePath
                int pathLength = ePath.length();
                int wz = Collections.binarySearch(childrens, currentPath.substring(pathLength+1));
                beforePath = ePath+"/"+childrens.get(wz -1);
                threadLocal.set(beforePath);
            }
            return false;
        }catch (Exception e){
            return false;
        }
    }

    @Override
    public boolean releaseLock(LockConfigBean lockConfigBean) throws Exception {
        ZkClient zkClient = zkClientUtils.getZkClient();
        if(zkClient != null){
            //删除临时节点,注意这里不能删除ePath了,因为ePath有子节点
            zkClient.delete(currentPath);
            zkClient.close();
            System.out.println(Thread.currentThread().getName()+"释放锁成功");
        }
        return true;
    }

    @Override
    public boolean getLock(LockConfigBean lockConfigBean) throws Exception {

        if(tryLock(lockConfigBean)){
            System.out.println(Thread.currentThread().getName()+"获取锁成功.");
            return true;
        }else {
            System.out.println(Thread.currentThread().getName()+"获取锁失败.");
            //获取锁失败就等待
            waitLock(lockConfigBean);
            //递归重新尝试获取锁
            return getLock(lockConfigBean);
        }
    }

    @Override
    public void waitLock(LockConfigBean lockConfigBean) throws Exception {
        IZkDataListener iZkDataListener = new IZkDataListener() {
            @Override
            public void handleDataChange(String s, Object o) throws Exception {
            }

            @Override
            public void handleDataDeleted(String s) throws Exception {
                if(null != countDownLatch){
                    countDownLatch.countDown();
                }
            }
        };
        ZkClient zkClient = zkClientUtils.getZkClient();
        //监听前一个节点的变化
        String beforePathx = threadLocal.get();
        zkClient.subscribeDataChanges(beforePathx,iZkDataListener);
        if(zkClient.exists(beforePathx)){
            countDownLatch = new CountDownLatch(1);
            try {
                countDownLatch.await();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        zkClient.unsubscribeDataChanges(beforePathx,iZkDataListener);
    }

    /**
     * 获取临时节点
     * @param lockConfigBean
     * @return
     */
    private String getPersist(LockConfigBean  lockConfigBean){
        return  "/"+lockConfigBean.getGroup()+"-"+lockConfigBean.getAppName()+"-"+lockConfigBean.getLockPath();
    }
}

五、实验main方法

package com.coderman.zookeeper.clusterdemo.distributelockdemo;

/**
 * @description:
 * @author: Fanchunshuai
 * @time: 2020/2/15 18:45
 *
 * 参考:https://blog.csdn.net/hongtaolong/article/details/88898875
 */
public class DistributeLockDemo {
    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            /**
             * 这里的10个线程你可以认为是10服务节点,不断的竞争锁
             * 因此案例代码启动之后10个线程会不断抢占执行演练
             */
            new Thread(new LockDemoRunnable()).start();
        }
    }

    static class LockDemoRunnable implements Runnable {

        @Override
        public void run() {
            LockConfigBean configBean = new LockConfigBean();
            configBean.setAppName("zhifuapp");
            configBean.setGroup("company");
            configBean.setWaitTime(20L);
            configBean.setLockPath("payforx");
            //下面注释的地方是常规方法实现的锁
            //ZKLock zkLock = new SimpleLockImpl();
            ZKLock zkLock = new HighPerformanceLockImpl();
            try {
                boolean x = zkLock.getLock(configBean);
                //时间延长看效果明显
                Thread.sleep(5000L);
                //这里某个线程释放锁的时候,其他线程都会进行监听,从而出现惊群效应
                zkLock.releaseLock(configBean);

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

总结:
zookeeper实现分布式锁有两种方式,下面做一下总结
一、普通版本的分布式锁
1.可以实现对临界资源的竞争实现分布式的控制,如果去掉重试机制的话则与redis实现的类似
2.普通版本的分布式锁只需要创建临时节点并与对应的session,加上节点监听即可。实现相对简单
3.正是由于简单,则会出现一旦某个节点释放了锁,其他节点都会抢夺锁,造成惊群效应。
4.当然这种简单版本的分布式锁适用于集群数不是很多,并发也不太高的情况。
二、高性能的分布式锁
1.高性能的分布式锁实现比较复杂,针对惊群效应进行了专门的优化。
2.实现需要几个核心点:
1.创建一个持久化节点
2.创建持久化节点下的临时顺序节点
3.获取锁失败后加入排队监听,并对所有临时顺序节点进行排序

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!