如何基于Zookeeper实现分布式锁,手把手教程来啦~

1 前言

随着时代的发展,现在互联网流量越来越大,多线程高并发下共享资源的安全性越来越重要,那么如何保证共享资源的安全呢?

如果单机环境可能会想到加synchronized关键字上锁。如果是分布式环境下,那么这种方案就显得力不从心了。那么,如何实现共享资源的安全访问呢?毫无疑问,肯定是加分布式锁。

这里,笔者以Zookeeper为例,介绍下,如何实现分布式锁。

2 基于原生的zookeeper客户端实现分布式锁

2.1 添加maven依赖

<!--zookeeper的官方客户端jar包依赖-->
<dependency>
     <groupId>org.apache.zookeeper</groupId>
     <artifactId>zookeeper</artifactId>
     <version>3.5.5</version>
</dependency>

2.2 添加监听器

当前节点如果是最小节点,就获取到了锁,如果不是最小,需要监听它的上一个节点,如果上一个节点删除了,以此类推,最后当前节点,如果是最小的节点,就获取到了锁。

【代码实现示例】

package cn.smilehappiness.distributelock.zookeeper;

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

import java.util.concurrent.CountDownLatch;

/**
 * <p>
 * 监听器,监听接节点事件
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/8/2 21:57
 */
public class ZookeeperLockWatcher implements Watcher {

    private CountDownLatch countDownLatch;

    public ZookeeperLockWatcher(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void process(WatchedEvent event) {
        //如果是节点删除事件
        if (event.getType() == Event.EventType.NodeDeleted) {
            //倒计数器减1
            countDownLatch.countDown();
        }
    }
}

2.3 实现分布式锁

思路: 基于有序临时节点来实现分布式锁

【代码示例】

package cn.smilehappiness.distributelock.zookeeper;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

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

/**
 * 基于原生的zookeeper客户端API实现分布式锁锁
 * 思路:基于有序临时节点来实现分布式锁
 */
public class ZookeeperLock {

    //zookeeper原生客户端对象
    private ZooKeeper zooKeeper;

    private String zkAddress = "127.0.0.1:2181";

    //分布式锁的根节点的名称
    private String lockRootName = "/locks";

    //锁节点的名称
    private String lockName;

    //当前的锁节点名称
    private String currentLockName;

    private static final int sessionTimeout = 10000;

    //默认的节点的数据
    private static final byte[] bytes = new byte[0];

    //倒计数器
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    /**
     * 构造方法
     *
     * @param lockName
     */
    public ZookeeperLock(String lockName) {
        //锁节点的名称通过构造方法初始化
        this.lockName = lockName;
        try {
            //建立zookeeper连接
            zooKeeper = new ZooKeeper(zkAddress, sessionTimeout, (WatchedEvent event) -> {
                if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
                    //zookeeper连上了
                    countDownLatch.countDown();
                }
            });
            //等待,阻塞
            countDownLatch.await();

            //如果阻塞结束,说明连接zookeeper成功,我们创建一个锁的根节点 lockRootName = "/locks";
            /**
             * --/locks  --业务区分
             *   --storeLock000000001
             *   --storeLock000000001
             *   --storeLock000000001
             *   ......
             */
            Stat stat = zooKeeper.exists(lockRootName, false);
            //如果锁的根节点不存在
            if (null == stat) {
                //根节点持久化,acl的开放的
                zooKeeper.create(lockRootName, bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * zookeeper分布式锁:加锁 (获取分布式锁)
     */
    public void lock() {
        try {
            /**
             * 返回:
             * /locks/lockName0000001
             * /locks/lockName0000002
             * /locks/lockName0000003
             * .........
             */
            String myNode = zooKeeper.create(lockRootName + "/" + lockName, bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            //拿到根节点下的所有临时有序子节点
            List<String> subNodes = zooKeeper.getChildren(lockRootName, false);

            //把所有子节点排序一下 (默认字典排序,0-9,a-z)
            TreeSet<String> sortNodes = new TreeSet<>();
            for (String node : subNodes) {
                // /locks/lockName0000001
                sortNodes.add(lockRootName + "/" + node);
            }

            //从排好顺序的set集合中取第一个节点,它是节点编号最小的
            String minNode = sortNodes.first();

            System.out.println("当前的myNode=" + myNode);
            System.out.println("最小节点minNode=" + minNode);

            //获取一下前一个节点,获取指定节点的前一个节点
            // myNode: /locks/lockName0000003,  (/locks/lockName0000002、/locks/lockName0000001)
            String preNode = sortNodes.lower(myNode);
            System.out.println("前一个节点preNode=" + preNode);

            //最小节点能拿到分布式锁
            if (myNode.equals(minNode)) {
                //当前进来的这个线程所创建的myNode就是分布式锁节点
                currentLockName = myNode;
                //已经获取到分布式锁
                return;
            }

            //其他进来的线程没有拿到分布锁,因为它所创建的节点不是最小的,那么他就监听前一个节点的删除事件
            //一个并发线程工具类:倒计数器
            CountDownLatch countDownLatch = new CountDownLatch(1);
            //判断字符串是否为null
            if (null != preNode) {
                //如果前一个节点不是空的,那么我就要监听前一个节点,当它删除时触发我的监听事件
                Stat stat = zooKeeper.exists(preNode, new ZookeeperLockWatcher(countDownLatch));

                if (null != stat) {
                    //阻塞,等待,那什么时候等待结束,就看前一个节点被监听器监听到删除事件,此时的阻塞等待结束
                    countDownLatch.await();

                    //又拿到分布式锁
                    currentLockName = myNode;

                    //倒计数对象置为null,促进垃圾回收
                    countDownLatch = null;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * zookeeper分布式锁:解锁
     */
    public void unLock() {
        //解锁主要就是把当前锁的节点从zookeeper中删除
        //解锁:另外一个做法是直接关闭zookeeper客户端(问题就是:你下次还要用锁,那就需要重新再建立zookeeper连接)
        try {
            if (currentLockName != null) {
                //版本号 -1 表示任何版本,不需要匹配版本,不管你是什么版本,我都可以删除
                zooKeeper.delete(currentLockName, -1);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

2.4 功能测试

/**
     * <p>
     * 分布式锁测试
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/8/2 22:10
     */
    private void testDistributedLock() {
        //使用原生zookeeper客户端api,底层创建zookeeper连接、创建锁的根节点
        ZookeeperLock lock = new ZookeeperLock("storeLock");

        try {
            //获取分布式锁,然后下面的业务代码就会按顺序排队执行
            lock.lock();
            //TODO 拿到redis分布式锁,执行业务代码
            // ......

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放分布式锁
            lock.unLock();
        }
    }

3 基于原生的ZkClient客户端实现分布式锁

3.1 添加maven依赖

<!--zkclient客户端的jar包依赖-->
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.11</version>
</dependency>

3.2 添加监听器

当前节点如果是最小节点,就获取到了锁,如果不是最小,需要监听它的上一个节点,如果上一个节点删除了,以此类推,最后当前节点,如果是最小的节点,就获取到了锁。

【代码实现示例】

package cn.smilehappiness.distributelock.zkclient;

import org.I0Itec.zkclient.IZkDataListener;

import java.util.concurrent.CountDownLatch;

/**
 * <p>
 * 对节点的监听,当前一个节点被删除的时候,触发节点删除事件的监听
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/8/2 21:56
 */
public class ZkClientLockWatcher implements IZkDataListener {

    private CountDownLatch countDownLatch;

    public ZkClientLockWatcher(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void handleDataChange(String dataPath, Object data) throws Exception {
    }

    /**
     * 监听节点删除事件
     *
     * @param dataPath
     * @throws Exception
     */
    @Override
    public void handleDataDeleted(String dataPath) throws Exception {
        //节点删除,把倒计数器减1
        countDownLatch.countDown();
    }
}

3.3 实现分布式锁

【代码示例】

package cn.smilehappiness.distributelock.zkclient;

import org.I0Itec.zkclient.ZkClient;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;

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

/**
 * 基于zkClient第三方客户端实现分布式锁
 * 思路:基于有序临时节点来实现分布式锁
 */
public class ZkClientLock {

    //zkclient第三档客户端对象
    private ZkClient zkClient;

    private String zkAddress = "127.0.0.1:2181";

    //分布式锁的根节点的名称
    private String lockRootName = "/locks";

    //锁节点的名称
    private String lockName;

    //当前的锁节点名称
    private String currentLockName;

    private static final int sessionTimeOut = 10000;

    private static final int connectionTimeOut = 25000;

    //默认的节点的数据
    private static final byte[] bytes = new byte[0];

    //倒计数器
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    /**
     * 构造方法
     *
     * @param lockName
     */
    public ZkClientLock(String lockName) {
        //锁节点的名称通过构造方法初始化
        this.lockName = lockName;
        try {
            //建立zookeeper连接
            zkClient = new ZkClient(zkAddress, sessionTimeOut, connectionTimeOut);

            //创建zkclient对象完成,说明连接zookeeper成功,我们创建一个锁的根节点 lockRootName = "/locks";
            /**
             * --/locks  --业务区分
             *   --storeLock000000001
             *   --storeLock000000001
             *   --storeLock000000001
             *   ......
             */
            boolean isExists = zkClient.exists(lockRootName);
            //如果锁的根节点不存在
            if (!isExists) {
                //根节点持久化,acl的开放的
                zkClient.create(lockRootName, bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * zookeeper分布式锁:加锁 (获取分布式锁)
     */
    public void lock() {
        try {
            /**
             * 返回:
             * /locks/lockName0000001
             * /locks/lockName0000002
             * /locks/lockName0000003
             * .........
             */
            String myNode = zkClient.create(lockRootName + "/" + lockName, bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            //拿到根节点下的所有临时有序子节点
            List<String> subNodes = zkClient.getChildren(lockRootName);

            //把所有子节点排序一下 (默认字典排序,0-9,a-z)
            TreeSet<String> sortNodes = new TreeSet<String>();
            for (String node : subNodes) {
                //    /locks/lockName0000001
                sortNodes.add(lockRootName + "/" + node);
            }

            //从排好顺序的set集合中取第一个节点,它是节点编号最小的
            String minNode = sortNodes.first();

            System.out.println("当前的myNode=" + myNode);
            System.out.println("最小节点minNode=" + minNode);

            //获取一下前一个节点,获取指定节点的前一个节点
            // myNode: /locks/lockName0000003,  (/locks/lockName0000002、/locks/lockName0000001)
            String preNode = sortNodes.lower(myNode);
            System.out.println("前一个节点preNode=" + preNode);

            //最小节点能拿到分布式锁
            if (myNode.equals(minNode)) {
                //当前进来的这个线程所创建的myNode就是分布式锁节点
                currentLockName = myNode;
                //已经获取到分布式锁
                return;
            }

            //其他进来的线程没有拿到分布锁,因为它所创建的节点不是最小的,那么他就监听前一个节点的删除事件
            //一个并发线程工具类:倒计数器
            CountDownLatch countDownLatch = new CountDownLatch(1);
            //判断字符串是否为null
            if (null != preNode) {
                //如果前一个节点不是空的,那么我就要监听前一个节点,当它删除时触发我的监听事件
                boolean isExists = zkClient.exists(preNode);

                if (isExists) {
                    //监听前一个节点
                    zkClient.subscribeDataChanges(preNode, new ZkClientLockWatcher(countDownLatch));

                    countDownLatch.await();

                    //又拿到分布式锁
                    currentLockName = myNode;

                    //倒计数对象置为null,促进垃圾回收
                    countDownLatch = null;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * zookeeper分布式锁:解锁
     */
    public void unLock() {
        //解锁主要就是把当前锁的节点从zookeeper中删除
        //解锁:另外一个做法是直接关闭zookeeper客户端(问题就是:你下次还要用锁,那就需要重新再建立zookeeper连接)
        try {
            if (currentLockName != null) {
                //版本号 -1 表示任何版本,不需要匹配版本,不管你是什么版本,我都可以删除
                zkClient.delete(currentLockName, -1);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

3.4 功能测试

 /**
     * <p>
     * 分布式锁测试
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/8/2 22:30
     */
    private void testDistributedLock() {
        //使用原生zookeeper客户端api,底层创建zookeeper连接、创建锁的根节点
        ZookeeperLock lock = new ZookeeperLock("storeLock");

        try {
            //获取分布式锁,然后下面的业务代码就会按顺序排队执行
            lock.lock();
            //TODO 拿到redis分布式锁,执行业务代码
            // ......

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放分布式锁
            lock.unLock();
        }
    }

4 基于Curator客户端实现分布式锁(推荐)

由于Curator客户端给我们提供了现成的分布式互斥锁来实现分布式锁,所以我们不必自己开发。工作中为了效率还是建议使用Curator客户端。

4.1 添加maven依赖

<!-- curator-recipes -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.2.0</version>
</dependency>

4.2 实现分布式锁

这里采用共享可重入锁实现,代码示例如下:

package cn.smilehappiness.distributelock.curator;

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;
import org.apache.curator.retry.RetryNTimes;
import org.springframework.util.Assert;

import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 基于Curator客户端实现分布式锁
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/8/2 22:28
 */
public class CuratorClientLock {

    /**
     * 客户端连接地址
     */
    private static final String ZK_ADDRESS = "127.0.0.1:2181";
    /**
     * 客户端根节点
     */
    private static final String LOCK_NODE = "/lockRoot";
    /**
     * 会话超时时间
     */
    private final int SESSION_TIMEOUT = 30 * 1000;
    /**
     * 连接超时时间
     */
    private final int CONNECTION_TIMEOUT = 5 * 1000;
    /**
     * 创建zookeeper连接实例
     */
    private static CuratorFramework client = null;
    private static CuratorFramework client2 = null;

    /**
     * 重试策略
     * baseSleepTimeMs:初始的重试等待时间,单位毫秒
     * maxRetries:最多重试次数
     */
    private static final RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
    /**
     * 重试策略
     * n:最多重试次数
     * sleepMsBetweenRetries:重试时间间隔,单位毫秒
     */
    private static final RetryPolicy retry = new RetryNTimes(3, 2000);

    static {
        // 创建Curator连接对象
        connectCuratorClient();
    }

    /**
     * <p>
     * 创建Curator连接对象
     * <p/>
     *
     * @param
     * @return
     * @Date 2020/6/21 12:29
     */
    public static void connectCuratorClient() {
        //老版本的方式,创建zookeeper连接客户端
        client = CuratorFrameworkFactory.builder()
                .connectString(ZK_ADDRESS)
                .sessionTimeoutMs(5000)
                .connectionTimeoutMs(10000)
                .retryPolicy(retry)
                .build();

        //创建zookeeper连接,新版本
        client2 = CuratorFrameworkFactory.newClient(ZK_ADDRESS, retry);

        //启动客户端(Start the client. Most mutator methods will not work until the client is started)
        client.start();
        client2.start();

        System.out.println("zookeeper初始化连接成功:" + client);
        System.out.println("zookeeper初始化连接成功:" + client2);
    }

    public static void main(String[] args) throws Exception {
        CuratorClientLock curatorClientLock = new CuratorClientLock();
        //测试1
        test1();

        //测试2
        sharedReentrantLock();
    }

    private static void test1() throws Exception {
        InterProcessMutex lock = new InterProcessMutex(client, LOCK_NODE);
        try {
            // 方式一:直接获取锁
            lock.acquire();

            // 方式二:获取锁,60秒如果获取不到,超时(Acquire the mutex - blocks until it's available or the given time expires)
            if (lock.acquire(60, TimeUnit.MINUTES)) {

            }

            //TODO 获取到分布式锁后,执行业务处理

        } finally {
            if (lock.isAcquiredInThisProcess()) {
                lock.release();
            }
        }
    }

    public static void sharedReentrantLock() throws Exception {
        // 创建可重入锁
        InterProcessLock lock = new InterProcessMutex(client, LOCK_NODE);
        // lock2 用于模拟其他客户端
        InterProcessLock lock2 = new InterProcessMutex(client2, LOCK_NODE);

        // lock 获取锁
        lock.acquire();
        try {
            // lock 第二次获取锁
            lock.acquire();
            //TODO 获取到分布式锁后,执行业务处理

            try {
                // lock2 超时获取锁, 因为锁已经被 lock 客户端占用, 所以获取失败, 需要等 lock 释放
                System.out.println(lock2.acquire(2, TimeUnit.SECONDS));
                ;
            } finally {
                lock.release();
            }
        } finally {
            // 重入锁获取与释放需要一一对应, 如果获取 2 次, 释放 1 次, 那么该锁依然是被占用, 如果将下面这行代码注释, 那么会发现下面的 lock2 获取锁失败
            lock.release();
        }

        // 在 lock 释放后, lock2 能够获取锁
        Assert.isTrue(lock2.acquire(2, TimeUnit.SECONDS));
        lock2.release();
    }

}

是不是还是Curator客户端实现起来更简单呢,不用自己实现,不用监听删除事件。

好啦,本篇就总结到这里了!

参考资料链接:
https://www.jianshu.com/p/6618471f6e75
https://www.jianshu.com/p/31335efec309

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家评论,一起探讨,代码如有问题,欢迎各位大神指正!

给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值