分布式锁-使用Zookeeper实现分布式锁

使用Zookeeper实现分布式锁

目前,分布式应用渐渐取代了传统的单体应用,也有越来越多的程序员投入到分布式应用开发中。既然是分布式开发,肯定会遇到共享资源的访问(修改)问题。单体应用中多线程访问(修改)共享资源,我们想到的解决方案是synchronized、ReentrantLock,以保证数据的安全性和一致性。那么问题来了,分布式应用如何实现分布式锁呢?

好慌,打开浏览器百度一波,我们得到的答案如下:

  1. 基于数据库实现分布式锁
  2. 基于Redis实现分布式锁
  3. 基于ZooKeeper实现分布式锁

开心,原来分布式锁的实现已经有了成熟的解决方案。那么,我们需要做的就是用代码去实现这些方案。笔主在项目开发过程中,也遇到过共享资源的访问(修改)问题。当时为了快速解决这个问题,选用了第二种方案,基于Redis实现分布式锁。为什么选用了Redis?理由如下:

  1. 生产环境中已有基于Redis实现分布式锁的案例
  2. 考虑到Redis单线程,顺序处理命令的特性

闲下来的时候,还是想用不同的方案去解决同一个问题。那么本篇文章的主题来了,那就是使用ZooKeeper实现分布式锁。

什么是ZooKeeper?

这里笔主就不瞎BB了,直接借用百度百科对ZooKeeper下的定义。
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

简单理解,ZooKeeper将复杂的服务封装为简单的API,ZooKeeper集群保证了其可以提供稳定的服务。

那么,如何基于ZooKeeper实现分布式锁呢?思路如下:

  1. 在ZooKeeper的实例上创建一个永久节点作为根节点;
  2. 在根节点下创建一系列的临时顺序节点;
  3. 当某个临时顺序节点需要访问(修改)共享资源时,要求该节点获取到锁(其实就是访问共享资源的资格)?那么如何判定该节点能不能获取到锁呢?判定思路如下:
    3.1. 获取根节点下的所有临时顺序节点,对所有临时顺序节点排序。
    3.2. 如果当前节点是最小的那个临时顺序节点,就获取到锁。
    3.2.如果当前节点不是最小的那个临时顺序节点,那么就监听当前节点的前一个节点的删除事件,当前节点的前一个节点删除时,再去竞争锁。

这个实现思路主要依赖了ZooKeeper的哪些性质呢?

  1. ZooKeeper可以创建节点,并且节点有四种类型PERSISTENT(永久)、PERSISTENT_SEQUENTIAL(永久有序)、EPHEMERAL(临时)、EPHEMERAL_SEQUENTIAL(临时有序)。
package org.apache.zookeeper;
/***
 2.  CreateMode value determines how the znode is created on ZooKeeper.
 */
public enum CreateMode {

    PERSISTENT (0, false, false),

    PERSISTENT_SEQUENTIAL (2, false, true),

    EPHEMERAL (1, true, false),

    EPHEMERAL_SEQUENTIAL (3, true, true);
}

2.ZooKeeper的事件监听特性。当ZooKeeper客户端访问节点时,可以对节点设置事件监听,当节点创建、删除、数据变化、子节点变化时,会通知监听的客户端。

/**
 1. Enumeration of types of events that may occur on the ZooKeeper
  */
 public enum EventType {
            None (-1),
            NodeCreated (1),
            NodeDeleted (2),
            NodeDataChanged (3),
            NodeChildrenChanged (4);
 }

吧啦吧啦讲了这么多,现在上具体的实现代码。

package com.example.demo1.distribute.lock;

import java.io.IOException;

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

public class ZKClient {

    private static ZooKeeper instance = null;
    //连接地址
    private final static String CONNECT_STRING ="127.0.0.1:2181";
    //会话超时时间
    private final static int SESSION_TIMEOUT = 3000;
    //私有的构造方法
    private ZKClient () {}

    /**
     * 获取连接实例
     * @return
     */
    public static ZooKeeper getInstance() {
        if (instance == null) {
            synchronized (ZKClient.class) {
                if (instance == null) {
                    try {
                        instance = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, new Watcher(){
                            @Override
                            public void process(WatchedEvent event) {

                            }
                        });
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return instance;
    }
}

ZKClient这个类便于获取Zookeeper的连接实例,也就是客户端。使用单例模式实现,在分布式应用中,每一个应用使用一个客户端和ZooKeeper服务交互即可。

package com.example.demo1.distribute.lock;

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

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * ZK实现分布式锁
 * @author zhaoheng
 * @date   2018年8月3日
 */
public class DistributeLock {

    private static final Logger LOG = LoggerFactory.getLogger(DistributeLock.class);

    //根节点
    private static final String ROOT_NODE_LOCK = "/ROOT_LOCK";

    private ZooKeeper zooKeeper;

    //当前节点锁的id
    private String currentLockId;

    //节点存放的数据
    private final static String DATA = "node";

    private final CountDownLatch countDownLatch = new CountDownLatch(1);

    public DistributeLock () {
        //初始化连接实例
        this.zooKeeper = ZKClient.getInstance();
    }

    /**
     * 检查根节点
     */
    public void checkRootNode () {
        try {
            //先判断根节点是否存在
            Stat stat = zooKeeper.exists(ROOT_NODE_LOCK, false);
            if (stat == null) {
                //根节点不存在则创建根节点
                zooKeeper.create(ROOT_NODE_LOCK, DATA.getBytes(), 
                        ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            LOG.error("创建根节点出现异常:", e);
        }
    }

    /***
     * 获取分布式锁
     * @return
     */
    public synchronized boolean lock () {
        try {
            //在根节点下创建临时顺序节点
            currentLockId = zooKeeper.create(ROOT_NODE_LOCK + "/", DATA.getBytes(), 
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            LOG.info("线程{}创建了节点{}", Thread.currentThread().getName(), currentLockId);
            //获取根节点下的子节点集合
            List<String> childNodeList = zooKeeper.getChildren(ROOT_NODE_LOCK, true);
            //子节点集合排序
            Collections.sort(childNodeList);
            //获取到第一个节点
            String firstNode = childNodeList.get(0);
            String currentNode = currentLockId.substring(currentLockId.lastIndexOf("/") + 1);
            //如果当前子节点就是第一个子节点
            if (currentNode.equals(firstNode)) {
                LOG.info("线程{}获取到分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
                return true;
            }
            int index = childNodeList.indexOf(currentNode);
            LOG.info("线程{}对应的子节点不是第一个节点,当前子节点{}", Thread.currentThread().getName(), currentLockId);
            if (index > 0) {
                //获取当前子节点的前一个节点
                String preNode = childNodeList.get(index - 1);
                LOG.info("线程{}获取当前子节点的前一个节点,当前子节点{},前一个节点{}", Thread.currentThread().getName() , currentLockId, preNode);
                zooKeeper.exists(ROOT_NODE_LOCK + "/" +preNode, new Watcher (){

                    @Override
                    public void process(WatchedEvent event) {
                        //监听前一个节点的删除事件
                        if (event.getType().equals(EventType.NodeDeleted)) {
                            LOG.info("当前节点{}监听到前一个节点{}删除事件", currentLockId, preNode);
                            countDownLatch.countDown();
                        }
                    }

                });
                countDownLatch.await();
                LOG.info("线程{}获取到分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
                return true;
            }
        } catch (Exception e) {
            LOG.error("获取分布式锁出现异常:", e);
        }
        return false;
    }

    /**
     * 释放分布式锁
     * @return
     */
    public synchronized boolean unlock () {
        try {
            LOG.info("线程{}开始释放分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
            //删除当前子节点
            zooKeeper.delete(currentLockId, -1);
            LOG.info("线程{}释放分布式锁成功,当前子节点{}", Thread.currentThread().getName(), currentLockId);
            return true;
        } catch (Exception e) {
            LOG.error("释放分布式锁出现异常:", e);
        }
        return false;
    }
}

DistributeLock类是基于ZooKeeper的具体实现,采用的就是刚才讨论的思路。

/**
 * 测试分布式锁
 * @author zhaoheng
 * @date   2018年8月8日
 */
public class TestDistributeLock {

    private static final Logger LOG = LoggerFactory.getLogger(TestDistributeLock.class);

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        //定义一个定长的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            executorService.execute( new Runnable() {
                DistributeLock distributeLock = null;
                @Override
                public void run() {
                    try {
                        distributeLock = new DistributeLock();
                        //检查根节点
                        distributeLock.checkRootNode();
                        countDownLatch.countDown();
                        countDownLatch.await();
                        //获取锁
                        distributeLock.lock();
                        //随机睡眠一段时间,模拟业务处理
                        Thread.sleep(random.nextInt(1000));
                    } catch (Exception e) {
                        LOG.error("处理业务出现异常:", e);
                    } finally {
                        if (distributeLock != null) {
                            //释放锁
                            distributeLock.unlock();
                        }
                    }
                }

            });
        }
    }
}

当然,少不了测试类。在测试类中,使用10个线程模拟竞争分布式锁。另外,也使用了Jdk并发包下的工具类CountDownLatch,10个线程并行执行。

2018-08-08 17:23:56.059 - 线程pool-2-thread-1创建了节点/ROOT_LOCK/0000000000
2018-08-08 17:23:56.091 - 线程pool-2-thread-8创建了节点/ROOT_LOCK/0000000001
2018-08-08 17:23:56.091 - 线程pool-2-thread-10创建了节点/ROOT_LOCK/0000000002
2018-08-08 17:23:56.092 - 线程pool-2-thread-2创建了节点/ROOT_LOCK/0000000003
2018-08-08 17:23:56.092 - 线程pool-2-thread-9创建了节点/ROOT_LOCK/0000000004
2018-08-08 17:23:56.093 - 线程pool-2-thread-4创建了节点/ROOT_LOCK/0000000005
2018-08-08 17:23:56.093 - 线程pool-2-thread-3创建了节点/ROOT_LOCK/0000000006
2018-08-08 17:23:56.093 - 线程pool-2-thread-7创建了节点/ROOT_LOCK/0000000007
2018-08-08 17:23:56.094 - 线程pool-2-thread-6创建了节点/ROOT_LOCK/0000000008
2018-08-08 17:23:56.097 - 线程pool-2-thread-5创建了节点/ROOT_LOCK/0000000009
2018-08-08 17:23:56.101 - 线程pool-2-thread-1获取到分布式锁,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:56.101 - 线程pool-2-thread-8对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:56.101 - 线程pool-2-thread-8获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000001,前一个节点0000000000
2018-08-08 17:23:56.101 - 线程pool-2-thread-10对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:56.101 - 线程pool-2-thread-10获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000002,前一个节点0000000001
2018-08-08 17:23:56.102 - 线程pool-2-thread-2对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:56.103 - 线程pool-2-thread-2获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000003,前一个节点0000000002
2018-08-08 17:23:56.103 - 线程pool-2-thread-3对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:23:56.103 - 线程pool-2-thread-3获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000006,前一个节点0000000005
2018-08-08 17:23:56.103 - 线程pool-2-thread-7对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:23:56.104 - 线程pool-2-thread-7获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000007,前一个节点0000000006
2018-08-08 17:23:56.103 - 线程pool-2-thread-4对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:56.105 - 线程pool-2-thread-4获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000005,前一个节点0000000004
2018-08-08 17:23:56.103 - 线程pool-2-thread-9对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:56.107 - 线程pool-2-thread-9获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000004,前一个节点0000000003
2018-08-08 17:23:56.109 - 线程pool-2-thread-5对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:23:56.109 - 线程pool-2-thread-5获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000009,前一个节点0000000008
2018-08-08 17:23:56.109 - 线程pool-2-thread-6对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:23:56.112 - 线程pool-2-thread-6获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000008,前一个节点0000000007
2018-08-08 17:23:57.059 - 线程pool-2-thread-1开始释放分布式锁,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:57.237 - 线程pool-2-thread-1释放分布式锁成功,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:57.237 - 当前节点/ROOT_LOCK/0000000001监听到前一个节点0000000000删除事件
2018-08-08 17:23:57.238 - 线程pool-2-thread-8获取到分布式锁,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.701 - 线程pool-2-thread-8开始释放分布式锁,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.844 - 当前节点/ROOT_LOCK/0000000002监听到前一个节点0000000001删除事件
2018-08-08 17:23:57.844 - 线程pool-2-thread-8释放分布式锁成功,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.844 - 线程pool-2-thread-10获取到分布式锁,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.032 - 线程pool-2-thread-10开始释放分布式锁,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.256 - 当前节点/ROOT_LOCK/0000000003监听到前一个节点0000000002删除事件
2018-08-08 17:23:58.257 - 线程pool-2-thread-10释放分布式锁成功,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.257 - 线程pool-2-thread-2获取到分布式锁,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:58.868 - 线程pool-2-thread-2开始释放分布式锁,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:59.119 - 当前节点/ROOT_LOCK/0000000004监听到前一个节点0000000003删除事件
2018-08-08 17:23:59.120 - 线程pool-2-thread-9获取到分布式锁,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.120 - 线程pool-2-thread-2释放分布式锁成功,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:59.230 - 线程pool-2-thread-9开始释放分布式锁,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.286 - 当前节点/ROOT_LOCK/0000000005监听到前一个节点0000000004删除事件
2018-08-08 17:23:59.286 - 线程pool-2-thread-9释放分布式锁成功,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.286 - 线程pool-2-thread-4获取到分布式锁,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.641 - 线程pool-2-thread-4开始释放分布式锁,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.675 - 当前节点/ROOT_LOCK/0000000006监听到前一个节点0000000005删除事件
2018-08-08 17:23:59.675 - 线程pool-2-thread-4释放分布式锁成功,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.676 - 线程pool-2-thread-3获取到分布式锁,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.214 - 线程pool-2-thread-3开始释放分布式锁,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.299 - 当前节点/ROOT_LOCK/0000000007监听到前一个节点0000000006删除事件
2018-08-08 17:24:00.299 - 线程pool-2-thread-3释放分布式锁成功,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.299 - 线程pool-2-thread-7获取到分布式锁,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.365 - 线程pool-2-thread-7开始释放分布式锁,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.428 - 当前节点/ROOT_LOCK/0000000008监听到前一个节点0000000007删除事件
2018-08-08 17:24:00.429 - 线程pool-2-thread-7释放分布式锁成功,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.429 - 线程pool-2-thread-6获取到分布式锁,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.538 - 线程pool-2-thread-6开始释放分布式锁,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.564 - 当前节点/ROOT_LOCK/0000000009监听到前一个节点0000000008删除事件
2018-08-08 17:24:00.564 - 线程pool-2-thread-6释放分布式锁成功,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.564 - 线程pool-2-thread-5获取到分布式锁,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:24:00.646 - 线程pool-2-thread-5开始释放分布式锁,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:24:00.674 - 线程pool-2-thread-5释放分布式锁成功,当前子节点/ROOT_LOCK/0000000009

观察测试结果,节点/ROOT_LOCK/0000000000是最小的临时顺序节点,该节点创建成功后就获取到了分布式锁。其他临时顺序节点则监听其前一个节点的删除事件,其前一个节点删除时,就去竞争分布式锁。
测试过程中也发现,10个线程并行竞争分布式锁时,执行checkRootNode()方法会有9个线程捕获到NodeExistsException,这是因为第一个线程创建根节点成功后,其他根节点再去创建根节点就失败了。

2018-08-08 17:23:55.993 - 创建根节点出现异常:
org.apache.zookeeper.KeeperException$NodeExistsException: KeeperErrorCode = NodeExists for /ROOT_LOCK
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:119)
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:51)
    at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:783)
    at com.example.demo1.distribute.lock.DistributeLock.checkRootNode(DistributeLock.java:57)
    at com.example.demo1.distribute.lock.TestDistributeLock$1.run(TestDistributeLock.java:32)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
2018-08-08 17:23:55.993 - 创建根节点出现异常:

至此,基于ZooKeeper实现分布式锁介绍完毕。笔主水平有限,笔误或者不当之处还请批评指正。

猜你喜欢

转载自blog.csdn.net/zhaoheng314/article/details/81512414