基于Zookeeper实现分布式锁

283 阅读16分钟

1.什么是分布式锁

在单体的应用开发场景中,涉及并发同步的时候,大家往往采用的是synchronized或者Lock的方式来解决多线程之间的同步问题。

但是在分布式集群工作的开发场景中,那么就需要一种更高级的锁机制,来处理这种跨JVM进程之间的数据同步问题,这就是分布式锁。

总结成一句话,用来解决分布式集群环境下的多线程同步问题,或多个跨JVM进程的同步问题。

2.公平锁与可重入锁

很经典的分布式锁时可重入的公平锁。那么,什么是可重入的公平锁?说实话,刚看到这个概念的时候有点懵,在此之间我是还没接触到这些锁机制的。可重入的公平锁,其实是具有两种锁机制的特性,即公平锁和可重入锁。

那么什么是公平锁,什么是可重入锁。建议可以直接看我的参考资料,我觉得他们举的例子,以及相关的图解都很清晰。这里补充下自己的观点。

所谓公平锁,就是每个线程,获取锁的这个过程,或者说获取锁的操作,看上去是公平的。(这里不要钻牛角尖,问什么是公平)。

参考资料中有这样一个举例,当我们去买东西的时候,遇到很多人在买,大家不约而同地进行排队。这时候,我要加入到排队的队列末尾进行排队,这对大家来说就是公平的,因为我们都默认遵循着“先来先得”。你要想快点买到,你就得付出更多的时间提早到现场。

假设我不进行排队,而选择直接插队,这很显然就会破坏了这种规则。对于那些先来但是一直买不到的人来说,这就是不公平的。

那么在分布式锁的实现中,公平锁体现在哪里。在基于Zookeeper实现的分布式锁中,每个想要操作共享资源之前,必须先获取一个锁的使用权,成功获取锁之后才能执行加锁和释放锁的操作。在加锁和释放锁的这个过程中,别的线程就得乖乖等待。

想要获取锁的线程,首先会在Zookeeper中创建一个临时有序节点,临时有序节点中维护了一个内部的序号,并且依次增加,这个序号是由Zookeeper内部进行维护的。所有想要获取锁的线程,都要首先在Zookeeper中创建一个临时有序节点。创建完成之后,由谁去获取锁呢?由节点序号最小的那一个线程去获取锁。其他没有获取到锁的线程,可以根据各自实际的业务场景,进行等待或者直接放弃锁的获取。

那么具有什么样的特性才叫做可重入锁呢?接着上述排队买东西那个例子,假设商家规定一个规则。所有排队的人的家人可以直接到排队的那个人那里,不用重新排在队尾。此时队列中的每一个属性单元,由个人变成了以家庭为单位进行的排队。假设此时轮到张三(这里的张三不特指某一个人,是一个用来举例的虚拟人物)买东西。然后他的家人来了,可以直接到张三所在的位置来进行选购,不用重新排队。

可重入锁在程序中的体现是什么?假设存在线程A,线程A已经通过创建Zookeeper的临时有序节点获取到分布式锁了。执行完一次之后,在某个时间段内,这个线程A还想再执行一次,这个时候,线程A不用再去重新创建Zookeeper节点参与分布式锁的竞争。而是直接去加锁和释放锁。总结成一句话,在某个线程获得分布式锁的时间段内,这个线程可以重复的加锁和解锁,不用重新排队。

3.基于Zookeeper的分布式锁的实现

在写代码之前有必要说明一下,我的代码实现是基于怎么样的一个业务场景。我实现的分布式锁主要面对两个业务场景:

第一个就是参考资料中的多个线程去竞争一个共享资源,然后分布式锁的存在是为了能够让程序在多线程环境下能够正常运行,得到正确的运行结果。

第二个场景就是我在工作中遇到的一个问题,我的应用被分别部署到了两台服务器上。然而我的应用中有一个定时任务,目的是从一个地方读取数据,然后存在在自己的数据库中。但是因为是两台机器上都运行着相同的程序,所以相同的定时任务就会执行两次,但是我这个同步的数据是不能重复的。假设在什么都不做的情况下,同样的定时任务代码会被执行两次,也就是会进行两次相同的入库操作,这就会导致我数据库中的数据是会出现重复。为了解决这一个问题,可以通过分布式锁进行控制,在执行定时任务的代码之前,先进行分布式锁的获取,获取到锁才行继续操作,另一个获取不到的,也不用等待,获取不到锁即视为此次不执行。

因为场景一和场景二要解决的问题不同,所以接下来会针对两个场景有不同的分布式锁的实现,但是总体上都是可重入的公平锁。

定义分布式锁接口DistributedLock:

package com.qingyuan.zookeeper_demo.util;

/**
 * 使用Zookeeper实现分布式锁
 *
 * @author qingyuan
 * @date 2022-02-25
 */
public interface DistributedLock {

    /**
     * 加锁
     *
     * @return
     * @throws Exception
     */
    Boolean lock();

    /**
     * 解锁
     *
     * @return
     */
    Boolean unlock();
}

场景一的分布式锁接口的具体实现:

package com.qingyuan.zookeeper_demo.util;

import org.apache.zookeeper.AddWatchMode;
import org.apache.zookeeper.CreateMode;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>
 * 基于Zookeeper实现分布式锁-可重入的公平锁
 * 假设当前场景:当前应用分别部署在两台服务器上,两台服务器在同一时间执行某个定时任务,需要获取到分布式锁才能进行业务操作
 * 两个场景,一个是之前设定的场景,一个是多个线程同时竞争,并且多个线程都要执行完毕才行
 * </p>
 *
 *  <p>
 *  目前存在一个问题:"前一个节点的路径为空,无法进行监听"。猜测是cpu指令重排问题,但是无法具体确定和解决
 *  </p>
 *
 *  <p>
 *  还有就是,当前场景下的,监听前一个节点的动作,似乎仅仅就是监听,而无其他动作。与上述描述为同一个问题
 *  </p>
 *
 *
 *
 * @author qingyuan
 *
 * @version 1.0
 * @date 2022-03-07
 */
public class ZkDistributedLock implements DistributedLock {

    Logger LOGGER = LoggerFactory.getLogger(ZkDistributedLock.class);


    /**
     * Zookeeper客户端
     */
    private ZooKeeper client = null;

    /**
     * Zk持久节点-分布式锁
     */
    private static final String ZK_NODE = "/test";

    /**
     * 创建子节点的前缀
     */
    private static final String LOCK_PREFIX = ZK_NODE + "/";

    /**
     * 连接超时时间,单位:毫秒
     */
    private static final Integer WAIT_TIME = 5000;

    private static final Integer SESSION_TIMEOUT = 300000;

    /**
     * 需要连接的Zookeeper服务端地址
     */
    private static String zookeeper_addr = "你的Zookeeper地址和端口";

    /**
     * 前一个节点的路径
     */
    private volatile String prior_path;

    /**
     * 当前创建节点路径
     */
    private String current_node_path = null;

    final AtomicInteger lockCount = new AtomicInteger(0);
    private Thread thread;


    /**
     * 无参构造方法,初始化zk客户端
     * 1.客户端连接Zookeeper服务端
     * 2.判断是否创建节点(分布式锁)
     * 3.如果没有创建节点就进行节点创建
     */
    public ZkDistributedLock() {
        connectionZkServer();
        try {
            //判断节点存在这个操作耗时很长
            Stat isExists = client.exists(ZK_NODE, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    //事件回调,这个回调只会触发一次,Zookeeper Watch使用 在返回不为空的时候才会回调这个Watcher
                    LOGGER.error("我是一条酸菜鱼");
                }
            });
            if (Objects.isNull(isExists)) {
                LOGGER.info("ZK_NODE不存在");
                //“主”节点不存在,则进行创建,不能级联创建?
                String createResultStr = client.create(
                        ZK_NODE,
                        "123".getBytes(),
                        ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            LOGGER.error("判断节点是否存在出现异常", e);
        }

    }

    /**
     * 初始化zk客户端
     */
    private void connectionZkServer() {
        try {
            client = new ZooKeeper(zookeeper_addr, SESSION_TIMEOUT, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    LOGGER.info("this is init Watch impl");
                }
            });
        } catch (IOException e) {
            LOGGER.error("connect zookeeper server fail", e);
        }

    }


    public static void main(String[] args) {
        ZkDistributedLock zkDistributedLock = new ZkDistributedLock();
    }


    /**
     * 加锁操作,每个线程调用相同的代码
     *
     * @return
     */
    @Override
    public Boolean lock() {
        //可重入锁就是可以多次加锁和释放锁,并且不一定要按照加锁-解锁,加锁-解锁的顺序?
        //添加可重入操作,确保同一线程可重复加锁,不用重新等待加锁
        //lockCount是单个JVM进程下,所有线程加锁的次数?
        synchronized (this){
            if(lockCount.get() == 0){
                //当前线程为第一个加锁的,默认成功,因为后面加锁不需要进行等待
                thread = Thread.currentThread();
                //加锁次数加一
                lockCount.incrementAndGet();
                //往下进行加锁操作
            }else{
                //如果lockCount不等于0,表示此前已经有线程进行加锁操作了
                //而thread变量保存的是上一个加锁的线程,判断当前线程是否和上一个线程是同一个线程
                //thread不为空的时候应该是已经加锁,但是没有释放锁的时候,在之后的释放锁操作应该会对这个变量进行操作
                if(!thread.equals(Thread.currentThread())){
                    return false;
                }
                lockCount.incrementAndGet();
                return true;
            }
        }



        Boolean islocked = false;
        //尝试去加锁
        islocked = tryLock();
        if (islocked) {
            LOGGER.info("分布式锁加锁成功:" + Thread.currentThread().getName());
            return true;
        }
        //如果加锁失败就要去等待,
        //循环等待的话,每个线程都要执行一次咯,就违背了我的业务场景,我的业务场景应该是只加锁一次,加锁失败就要等到下次了
        while (!islocked) {
            //等待,那其实这里也仅仅是监听而已,还是得自己不断地尝试加锁,这不就是羊群效应?
            await();
            //获得等待的子节点列表
            List<String> waiters = getWaiters();
            //判断能否加锁成功
            if (checkLocked(waiters, current_node_path)) {
                islocked = true;
            }
        }
        return islocked;
    }

    /**
     * 尝试加锁操作
     *
     * @return
     */
    public Boolean tryLock() {
        Boolean result = false;
        String tempNodePath = "";
        try {
            //创建临时有序节点
            tempNodePath = client.create(
                    LOCK_PREFIX,
                    "node".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            current_node_path = tempNodePath;
        } catch (Exception e) {
            LOGGER.error("创建子节点失败", e);
        }
        //获取子节点列表
        List<String> waiters = getWaiters();
        if (!Objects.isNull(waiters)) {
            //判断当前创建节点的序号是否是最小的
            if (checkLocked(waiters, tempNodePath)) {
                result = true;
                return result;
            }

            //如果当前创建节点的序号不是最小的,则会对前一个节点的路径进行保存,方便后续进行监听操作
            //获取当前创建的节点在子节点列表中的下标
            int index = Collections.binarySearch(waiters, tempNodePath);
            //前一个节点的路径
            if (1 <= index) {
                prior_path = ZK_NODE + "/" + waiters.get(index - 1);
            }

        }
        return result;
    }

    /**
     * 获取指定节点下的子节点列表
     *
     * @return
     */
    public List<String> getWaiters() {
        List<String> waiters = null;
        try {
            waiters = client.getChildren(ZK_NODE, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    LOGGER.info("获取ZK_NODE子节点列表成功");
                }
            });
        } catch (Exception e) {
            LOGGER.error("获取ZK_NODE子节点列表出错", e);
        }
        return waiters;
    }

    /**
     * 检查是否成功获取到分布式锁
     *
     * @return 返回一个Boolean值表示是否成功获得分布式锁 true 获得,false 未获得
     * @param waiters 子节点路径列表
     * @param tempNodePath 创建的当前节点路径
     */
    public Boolean checkLocked(List<String> waiters, String tempNodePath) {
        Boolean result = false;
        //排序
        Collections.sort(waiters);

        if (tempNodePath.equals(ZK_NODE + "/" + waiters.get(0))) {
            LOGGER.info("成功获取分布式锁,节点为{}", tempNodePath);
            result = true;
        }
        return result;
    }

    /**
     * 监听前一个节点的删除事件
     */
    public void await() {
        if (!StringUtils.hasText(prior_path)) {
            LOGGER.error("前一个节点的路径为空,无法进行监听");
            return;
        }
        //用CountDownLatch 来实现多个线程之间的同步,多线程同步也就是多线程通信
        final CountDownLatch latch = new CountDownLatch(1);

        //定义监听事件
        Watcher watcher = new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                LOGGER.info("监听到的变化,watchedEvent==" + watchedEvent);
                //CountDownLatch计数器减一
                latch.countDown();
            }
        };
        try {
            //第三个参数不知道啥意思
            client.addWatch(prior_path, watcher, AddWatchMode.PERSISTENT);
            /**
             * 当CountDownLatch减到0的时候,就会唤醒在CountDownLatch等待的线程
             * @param WAIT_TIME 延时唤醒时间
             * @param TimeUnit.SECONDS 时间单位 秒
             */
            latch.await(WAIT_TIME, TimeUnit.SECONDS);
        } catch (Exception e) {
            LOGGER.error("添加监听事件失败", e);
        }

    }

    /**
     * 释放锁
     *
     * @return 返回一个布尔值表示是否成功释放锁
     */
    @Override
    public Boolean unlock() {
        //只有加锁的线程才能进行解锁
        if(!thread.equals(Thread.currentThread())){
            return false;
        }
        //减少可重入的计数
        int newLockCount = lockCount.decrementAndGet();
        //计数不能小于0,小于0表示在操作之前为0,为0表示加锁的次数为0,则不可能进行锁的释放
        if(newLockCount < 0){
            throw new IllegalMonitorStateException("Lock count has gone negative for lock:");
        }
        //成功释放一次锁
        if(newLockCount != 0){
            return true;
        }

        //如果计数为0,则表示是“最后”一次释放锁,需要将相应的节点删除
        //其实,是不是可以直接断开连接即可?
        try{
            Stat exists = client.exists(current_node_path, false);
            if(!Objects.isNull(exists)){
                /**
                 * 删除节点
                 * @param path 需要删除的节点路径
                 * @param version -1 表示不需要管版本 path 匹配就可以执行 否则需要版本匹配,不然就会抛异常
                 */
                client.delete(current_node_path,-1);
                LOGGER.info("释放锁操作删除节点成功");
            }
        }catch (Exception e){
            LOGGER.error("释放锁删除节点出现异常",e);
        }
        return true;
    }
}

场景二的分布式锁的实现:

package com.qingyuan.zookeeper_demo.util;

import org.apache.zookeeper.AddWatchMode;
import org.apache.zookeeper.CreateMode;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>
 * 类描述:针对efb双机热备的场景下的分布式锁
 * </p>
 *
 *
 * <p>
 * 场景描述:在双机热备的场景下,两个服务器运行相同的服务,其中包括运行相同的定时任务。
 * 假设某个具体的定时任务要执行的操作是,从某个地方同步指定日期的数据到我们的数据库中,但是我们不希望这些数据冗余。
 * 在两个应用的定时任务同时运行的情况下,两个任务都会在相同时间节点去同步数据,并且写入数据库中,这就会造成数据库中的数据冗余
 * </p>
 *
 * <p>
 * 相比之前的分布式锁的实现,这个的实现,区别在于哪里
 * 1.获取分布式锁时,如果获取不到,不需要等待,直接放弃获取。如果需要等待的话,岂不是都要去执行一遍了,那这个分布式锁的意义也就没了。
 * </p>
 *
 *
 * @author qingyuan
 * @version 1.0
 * @date 2022-03-10
 */
public class ZkDistributedLockEfb implements DistributedLock {

    Logger LOGGER = LoggerFactory.getLogger(ZkDistributedLockEfb.class);

    /**
     * Zookeeper客户端
     */
    private ZooKeeper client = null;

    /**
     * Zk持久节点-分布式锁
     */
    private static final String ZK_NODE = "/efb";

    /**
     * 创建子节点的前缀
     */
    private static final String LOCK_PREFIX = ZK_NODE + "/";

    /**
     * 连接超时时间,单位:毫秒
     */
    private static final Integer WAIT_TIME = 5000;

    private static final Integer SESSION_TIMEOUT = 300000;

    /**
     * 需要连接的Zookeeper服务端地址
     */
    private static String zookeeper_addr = "你的Zookeeper地址和端口";

    /**
     * 前一个节点的路径
     */
    private volatile String prior_path;

    /**
     * 当前创建节点路径
     */
    private String current_node_path = null;

    final AtomicInteger lockCount = new AtomicInteger(0);
    private Thread thread;


    /**
     * 无参构造方法,初始化zk客户端
     * 1.客户端连接Zookeeper服务端
     * 2.判断是否创建节点(分布式锁)
     * 3.如果没有创建节点就进行节点创建
     */
    public ZkDistributedLockEfb() {
        connectionZkServer();
        try {
            //判断节点存在这个操作耗时很长
            Stat isExists = client.exists(ZK_NODE, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    //事件回调,这个回调只会触发一次,Zookeeper Watch使用 在返回不为空的时候才会回调这个Watcher
                    LOGGER.error("我是一条酸菜鱼");
                }
            });
            if (Objects.isNull(isExists)) {
                LOGGER.info("ZK_NODE不存在");
                //“主”节点不存在,则进行创建,不能级联创建?
                String createResultStr = client.create(
                        ZK_NODE,
                        "123".getBytes(),
                        ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            LOGGER.error("判断节点是否存在出现异常", e);
        }

    }

    /**
     * 初始化zk客户端
     */
    private void connectionZkServer() {
        try {
            client = new ZooKeeper(zookeeper_addr, SESSION_TIMEOUT, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    LOGGER.info("this is init Watch impl");
                }
            });
        } catch (IOException e) {
            LOGGER.error("connect zookeeper server fail", e);
        }

    }


    public static void main(String[] args) {
        ZkDistributedLock zkDistributedLock = new ZkDistributedLock();
    }


    /**
     * 加锁操作,每个线程调用相同的代码
     *
     * @return
     */
    @Override
    public Boolean lock() {
        //可重入锁就是可以多次加锁和释放锁,并且不一定要按照加锁-解锁,加锁-解锁的顺序?
        //添加可重入操作,确保同一线程可重复加锁,不用重新等待加锁
        //lockCount是单个JVM进程下,所有线程加锁的次数?
        synchronized (this){
            if(lockCount.get() == 0){
                //当前线程为第一个加锁的,默认成功,因为后面加锁不需要进行等待
                thread = Thread.currentThread();
                //加锁次数加一
                lockCount.incrementAndGet();
                //往下进行加锁操作
            }else{
                //如果lockCount不等于0,表示此前已经有线程进行加锁操作了
                //而thread变量保存的是上一个加锁的线程,判断当前线程是否和上一个线程是同一个线程
                //thread不为空的时候应该是已经加锁,但是没有释放锁的时候,在之后的释放锁操作应该会对这个变量进行操作
                if(!thread.equals(Thread.currentThread())){
                    return false;
                }
                lockCount.incrementAndGet();
                return true;
            }
        }



        Boolean islocked = false;
        //尝试去加锁
        islocked = tryLock();
        if (islocked) {
            LOGGER.info("分布式锁加锁成功:" + Thread.currentThread().getName());
            return true;
        }
        /**
        //如果加锁失败就要去等待,
        //循环等待的话,每个线程都要执行一次咯,就违背了我的业务场景,我的业务场景应该是只加锁一次,加锁失败就要等到下次了
        while (!islocked) {
            //等待,那其实这里也仅仅是监听而已,还是得自己不断地尝试加锁,这不就是羊群效应?
            await();
            //获得等待的子节点列表
            List<String> waiters = getWaiters();
            //判断能否加锁成功
            if (checkLocked(waiters, current_node_path)) {
                islocked = true;
            }
        }
         */
        return islocked;
    }

    /**
     * 尝试加锁操作
     *
     * @return
     */
    public Boolean tryLock() {
        Boolean result = false;
        String tempNodePath = "";
        try {
            //创建临时有序节点
            tempNodePath = client.create(
                    LOCK_PREFIX,
                    "node".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            current_node_path = tempNodePath;
        } catch (Exception e) {
            LOGGER.error("创建子节点失败", e);
        }
        //获取子节点列表
        List<String> waiters = getWaiters();
        if (!Objects.isNull(waiters)) {
            //判断当前创建节点的序号是否是最小的
            if (checkLocked(waiters, tempNodePath)) {
                result = true;
                return result;
            }

            //如果当前创建节点的序号不是最小的,则会对前一个节点的路径进行保存,方便后续进行监听操作
            //获取当前创建的节点在子节点列表中的下标
            int index = Collections.binarySearch(waiters, tempNodePath);
            //前一个节点的路径
            if (1 <= index) {
                prior_path = ZK_NODE + "/" + waiters.get(index - 1);
            }

        }
        return result;
    }

    /**
     * 获取指定节点下的子节点列表
     *
     * @return
     */
    public List<String> getWaiters() {
        List<String> waiters = null;
        try {
            waiters = client.getChildren(ZK_NODE, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    LOGGER.info("获取ZK_NODE子节点列表成功");
                }
            });
        } catch (Exception e) {
            LOGGER.error("获取ZK_NODE子节点列表出错", e);
        }
        return waiters;
    }

    /**
     * 检查是否成功获取到分布式锁
     *
     * @return 返回一个Boolean值表示是否成功获得分布式锁 true 获得,false 未获得
     * @param waiters 子节点路径列表
     * @param tempNodePath 创建的当前节点路径
     */
    public Boolean checkLocked(List<String> waiters, String tempNodePath) {
        Boolean result = false;
        //排序
        Collections.sort(waiters);

        LOGGER.info("===当前节点为:"+ZK_NODE + "/"+current_node_path);
        LOGGER.info("===子节点列表排序后的第一个节点为"+ZK_NODE + "/" +waiters.get(0));
        if (tempNodePath.equals(ZK_NODE + "/" + waiters.get(0))) {
            LOGGER.info("成功获取分布式锁,节点为{}", tempNodePath);
            result = true;
        }
        return result;
    }

    /**
     * 监听前一个节点的删除事件
     */
    public void await() {
        if (!StringUtils.hasText(prior_path)) {
            LOGGER.error("前一个节点的路径为空,无法进行监听");
            return;
        }
        //用CountDownLatch 来实现多个线程之间的同步,多线程同步也就是多线程通信
        final CountDownLatch latch = new CountDownLatch(1);

        //定义监听事件
        Watcher watcher = new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                LOGGER.info("监听到的变化,watchedEvent==" + watchedEvent);
                //CountDownLatch计数器减一
                latch.countDown();
            }
        };
        try {
            //第三个参数不知道啥意思
            client.addWatch(prior_path, watcher, AddWatchMode.PERSISTENT);
            /**
             * 当CountDownLatch减到0的时候,就会唤醒在CountDownLatch等待的线程
             * @param WAIT_TIME 延时唤醒时间
             * @param TimeUnit.SECONDS 时间单位 秒
             */
            latch.await(WAIT_TIME, TimeUnit.SECONDS);
        } catch (Exception e) {
            LOGGER.error("添加监听事件失败", e);
        }

    }

    /**
     * 释放锁
     *
     * @return 返回一个布尔值表示是否成功释放锁
     */
    @Override
    public Boolean unlock() {
        //只有加锁的线程才能进行解锁
        if(!thread.equals(Thread.currentThread())){
            return false;
        }
        //减少可重入的计数
        int newLockCount = lockCount.decrementAndGet();
        //计数不能小于0,小于0表示在操作之前为0,为0表示加锁的次数为0,则不可能进行锁的释放
        if(newLockCount < 0){
            throw new IllegalMonitorStateException("Lock count has gone negative for lock:");
        }
        //成功释放一次锁
        if(newLockCount != 0){
            return true;
        }

        //如果计数为0,则表示是“最后”一次释放锁,需要将相应的节点删除
        //其实,是不是可以直接断开连接即可?
        try{
            Stat exists = client.exists(current_node_path, false);
            if(!Objects.isNull(exists)){
                /**
                 * 删除节点
                 * @param path 需要删除的节点路径
                 * @param version -1 表示不需要管版本 path 匹配就可以执行 否则需要版本匹配,不然就会抛异常
                 */
                client.delete(current_node_path,-1);
                LOGGER.info("释放锁操作删除节点成功");
            }
        }catch (Exception e){
            LOGGER.error("释放锁删除节点出现异常",e);
        }
        return true;
    }
}

4.分布式锁的应用场景下的验证

场景一的验证:

package com.qingyuan.zookeeper_demo.util;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.CountDownLatch;

/**
 * <p>
 * zk实现分布式锁单元测试
 * </p>
 *
 * <p>
 * 目前存在一个问题:"前一个节点的路径为空,无法进行监听"。猜测是cpu指令重排问题,但是无法具体确定和解决
 * </p>
 *
 * @author qingyuan kaipengwu@foxmail.com
 * @version 1.0
 * @date 2022-03-10
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class ZkDistributedLockTest {

    /**
     * 计数器
     */
    private int count = 0;

    /**
     * 加锁前的测试
     *
     * @throws InterruptedException
     */
    @Test
    public void testLockBefore() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        //创建十个线程,每个线程都对变量进行加一,连续加十次
        for (int index = 0; index < 10; index++) {
            new Thread(() -> {
                for (int index2 = 0; index2 < 1000; index2++) {
                    count++;
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println("测试加锁之前程序运行结结果:"+count);
    }

    /**
     * 加锁后的测试
     * @throws InterruptedException
     */
    @Test
    public void testLockAfter()throws InterruptedException{
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int index = 0; index < 10; index++) {
            new Thread(() -> {
                ZkDistributedLock zkDistributedLock = new ZkDistributedLock();
                zkDistributedLock.lock();
                for (int index2 = 0; index2 < 1000; index2++) {
                    count++;
                }
                zkDistributedLock.unlock();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println("测试加锁之前程序运行结结果:"+count);
    }

    /**
     * 测试单线程环境下的加锁与释放锁
     *
     * @throws InterruptedException
     */
    @Test
    public void testSingleLockAfter() {
        ZkDistributedLock zkDistributedLock = new ZkDistributedLock();
        zkDistributedLock.lock();
        System.out.println("do something");
        zkDistributedLock.unlock();
    }

}

场景二的验证:

场景二涉及到自己创建的一些临时表的增删查改,这里有需要的小伙伴就自行去创建吧,这里展示定时任务的内容。然后把应用打包,分别部署到两台服务器上,然后监听日志,查看数据库的记录是否和预期一样。

package com.qingyuan.zookeeper_demo.task;

import com.qingyuan.zookeeper_demo.entity.User;
import com.qingyuan.zookeeper_demo.service.UserService;
import com.qingyuan.zookeeper_demo.util.ZkDistributedLockEfb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * efb场景下的定时任务
 *
 * @author qingyuan
 * @version 1.0
 * @date 2022-03-10
 */
@Component
public class EfbTask {

    Logger LOGGER = LoggerFactory.getLogger(EfbTask.class);

    @Autowired
    UserService userService;



    /**
     * 从第一分钟开始,每隔三分钟执行一次
     */
    @Scheduled(cron = "0 1/3 * * * ? ")
    public void synUserData(){
        LOGGER.info("===zookeeper_demo开始同步数据===");
        ZkDistributedLockEfb zkDistributedLockEfb = new ZkDistributedLockEfb();
        LOGGER.info("===zookeeper_demo开始获取分布式锁===");
        Boolean lock = zkDistributedLockEfb.lock();
        if(lock){
            //成功加锁才能进行操作
            User lastNewUserRecord = userService.getLastNewUserRecord();
            LOGGER.info("获取到的user对象{}"+lastNewUserRecord.toString());
            //为了程序不报错写的,没有实际意义
            lastNewUserRecord.setCreateDate(new Date());
            int i = userService.insertUserInToUserCopy(lastNewUserRecord);
            LOGGER.info("===zookeeper_demo定时任务插入影响的行数:"+i+"===");

        }
        /**
         * 释放锁过快会导致其他的应用在某个时间段内也会进行操作并且成功获取锁,这就违背了这个场景解决的初衷
         */
        try{
            //操作完成之后休眠20s再进行分布式锁的释放
            Thread.sleep(20*1000);
        }catch (Exception e){
            LOGGER.error("释放锁前进行休眠出现异常",e);
        }
        zkDistributedLockEfb.unlock();
        LOGGER.info("===zookeeper_demo释放分布式锁完毕===");

    }
}

5.Zookeeper分布式锁的优缺点

优点:可以有效的解决上述两个场景的问题,也可延伸至其他场景。

缺点:网上说,Zookeeper实现的分布式锁在性能上不是很好,在每次创建锁和释放锁的过程中,都要动态创建节点和销毁节点来实现锁功能。

然后我在编写代码的过程中,发现有些地方例如判断节点是否存在的时候耗时有点长,而且对会话的时长也有要求,会话超时时间如果过短的话,不足以支持执行完所有操作,就会报错。

6.本篇中程序还存在的问题

在场景一的验证当中,会出现:"前一个节点的路径为空,无法进行监听"。就是在预期之外的日志信息,但是结果是正确的。目前还无法彻底解决和定位是什么问题。猜测是cpu指令重排问题,但是无法具体确定和解决。

7.参考资料

7.1 Zookeeper 分布式锁 (图解+秒懂+史上最全)
7.2 阿里面试官:说一下公平锁和非公平锁的区别?
7.3 ZooKeeper客户端 zkCli.sh 节点的增删改查