Zookeeper实现分布式锁

Zookeeper系列介绍(持续更新

  1. 在Java中使用多线程编程,需要考虑多线程环境下程序执行结果的正确性,是否达到预期效果,因此需要在操作共享资源时引入锁,共享资源同一时刻只能由一个线程进行操作。
  2. Java提供了多种本地线程锁。例如synchronized锁,JUC包下提供的可重入锁ReentrantLock、读写锁ReentrantReadWriteLock等;
  3. Java本地锁适用于单机环境。在分布式环境下,存在多台服务器同时操作同一共享资源的场景时,服务器之间无法感知到Java本地锁的加锁状态,因此需要通过分布式锁来保证集群环境下执行任务的正确性;

常见分布式锁介绍

  • MySQL数据库中添加version字段实现乐观锁;
  • Redis的set命令(存在单点问题,若redis集群中某台机器宕机,可能引发加解锁混乱);
  • Redisson开源框架中实现的RedLock(解决了set方式实现引发的单点问题);
  • 通过Zookeeper官方API自主实现分布式锁;
  • Curator开源框架实现的Zookeeper分布式锁InterProcessMutex等;

本文根据Zookeeper官方API实现分布式锁,带大家了解Zookeeper的强大之处,后续各种锁的实现及原理也会带大家一一了解;

Zookeeper实现方式

Zookeeper中数据节点znode分为四种类型,实现分布式锁主要利用临时顺序节点。其特性具体介绍可见【https://www.jianshu.com/p/cbe5f0dd6cca】。

  • 实现思路
  1. 客户端中的线程需要加锁时,首先获取持久化锁节点路径下所有临时顺序节点,若当前线程创建的临时顺序节点为最小节点,则表示当前线程加锁成功;
  2. 若不是最小节点,则当前线程创建的节点监听比它小的最大节点,阻塞等待被监听节点的删除通知,待前置节点删除后,重新判断当前线程创建的节点是否为最小节点,若是,则加锁成功
  3. 若不是最小节点,则重复1、2步的操作,直到加锁成功;
  • 示例及分析

如下图所示,三个客户端线程分别对锁名为“test4”加锁,创建对应的三个临时顺序节点:client0000000000、client0000000001、client0000000002;

  1. 首先client0000000000获取锁,client0000000001监听client0000000000,client0000000002监听client0000000001;
  2. client0000000000节点删除后,通知client0000000001尝试获取锁;
  3. client0000000001节点删除后,通知client0000000002尝试获取锁;
    异常情况:若client0000000000持有锁时,client0000000001节点异常消失,那么client0000000002节点检测到client0000000000仍存在,则要监听client0000000000节点;
    临时顺序节点创建情况
  • 代码实现(可直接使用,拿走不谢)
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
 
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
 
@Service
public class ZkLockDemo implements InitializingBean, Watcher {
    private static Logger logger = LoggerFactory.getLogger(ZkLockDemo.class);
    private static volatile ZooKeeper zk;
    static String zkAddress = "127.0.0.1:2181";

    /**
     * 根节点
     */
    private String root = "/locksNode";
 
    /**
     * 存储当前线程创建的锁(临时顺序节点的全路径)
     */
    private ThreadLocal<List<String>> nodePathList = new ThreadLocal<>();
 
    public ZkLockDemo() {
    }
 
    @Override
    public void afterPropertiesSet() {
        createRootNode();
    }
 
    /**
     * 创建锁的持久化根节点
     */
    private void createRootNode() {
        try {
            if (StringUtils.isBlank(zkAddress)) {
                throw new NullPointerException("zooKeeper address conf error");
            }
            CountDownLatch countDownLatch = new CountDownLatch(1);
            //建立zk连接
            logger.info("开始连接zk", root);
            zk = new ZooKeeper(zkAddress, 10000, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        countDownLatch.countDown();
                    }
                }
            });
            //等待锁连接成功
            countDownLatch.await(10, TimeUnit.SECONDS);
            if (zk == null) {
                throw new NullPointerException("zooKeeper connect failure");
            }
            Stat stat = zk.exists(root, true);
            if (stat == null) {
                //创建持久化根节点
                zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                logger.info("根节点{}创建完成", root);
            } else {
                logger.info("根节点{}已存在,直接使用", root);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 监听zk是否需要重连接
     * @param watchedEvent
     */
    @Override
    public void process(WatchedEvent watchedEvent) {
        try {
            //zk的session过期时,重新创建连接
            if (watchedEvent.getState() == Event.KeeperState.Expired) {
                logger.info("zk连接过期,重新创建连接");
                zk.close();
                zk = null;
                createRootNode();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 创建具体的锁节点
     *
     * @param lockPath
     */
    private void createLockNode(String lockPath) {
        try {
            //判断指定锁路径是否存在,若不存在则创建
            Stat stat = zk.exists(lockPath, true);
            if (stat == null) {
                //创建持久化锁节点
                zk.create(lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                logger.info("锁路径创建成功:{}", lockPath);
            } else {
                logger.info("锁路径已经存在:{}", lockPath);
            }
        } catch (KeeperException.NodeExistsException e) {
            logger.error("node节点已经存在,本次创建失败:{}", e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 阻塞锁
     * @param lockName 锁名
     */
    public void lock(String lockName) {
        try {
            //创建锁目录
            String lockPath = root + "/" + lockName;
            createLockNode(lockPath);
 
            //当前线程创建的临时顺序节点
            String clientLockNode = zk.create(lockPath + "/client", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
 
            //获取当前临时顺序节点的前一个节点,若获取的前置节点为null,则表示当前节点获取到锁
            String preNode=getPreNode(lockPath,clientLockNode);
            CountDownLatch latch = new CountDownLatch(1);
            if(preNode!=null){
                //注册监听
                Stat lockStat = zk.exists(preNode, new LockWatcher(latch,lockPath,clientLockNode));
                if (lockStat != null) {
                    // 等待
                    latch.await();
                    latch = null;
                    addLock(clientLockNode);
                    logger.info("阻塞线程锁获取成功,锁路径为:{}", clientLockNode);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
 
    /**
     * 获取当前线程创建的临时顺序节点的前一个节点
     * @param lockPath 锁路径
     * @param clientLockNode 当前线程创建的临时顺序节点
     * @return 前一个临时顺序节点
     */
    private String getPreNode(String lockPath,String clientLockNode){
        String preNode=null;
        try {
            // 取出lockPath下所有子节点
            List<String> subNodes = zk.getChildren(lockPath, true);
            TreeSet<String> sortedNodes = new TreeSet<>();
            for (String node : subNodes) {
                sortedNodes.add(lockPath + "/" + node);
            }
            //获取最小临时顺序节点
            String minNode = sortedNodes.first();
            // 如果当前节点是最小节点,则表示取得锁
            if (clientLockNode.equals(minNode)) {
                addLock(clientLockNode);
                logger.info("锁获取成功,锁路径为:{}", clientLockNode);
            }else{
                //获取比当前节点小的最大节点进行监听
                preNode = sortedNodes.lower(clientLockNode);
                logger.info("阻塞等待获取锁,锁路径为:{},监听的前置节点为:{}", clientLockNode, preNode);
            }
        }catch (Exception e){
 
        }
        return preNode;
    }
 
    /**
     * 监听临时顺序节点是否被删除
     */
    class LockWatcher implements Watcher {
        private CountDownLatch latch = null;
        private String lockPath = null;
        private String clientLockNode = null;
        public LockWatcher(CountDownLatch latch,String lockPath,String clientLockNode) {
            this.latch = latch;
            this.lockPath=lockPath;
            this.clientLockNode=clientLockNode;
        }
        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == Event.EventType.NodeDeleted) {
                //若当前节点的前置节点被删除,需重新判断当前节点是否还存在前置节点
                //正常情况下前置节点删除,则表示当前节点获取锁
                //当前置节点没有获取锁,但是异常断连时,当前节点则需监听剩余的最大前置节点
                String preNode=getPreNode(lockPath,clientLockNode);
                if(preNode==null){
                    latch.countDown();
                }else{
                    try {
                        zk.exists(preNode, new LockWatcher(latch,lockPath,clientLockNode));
                    }catch (Exception e){
 
                    }
                }
            }
        }
    }
 
    /**
     * 尝试获取锁
     * @param lockName
     * @return
     */
    public boolean tryLock(String lockName) {
        try {
            //创建锁目录
            String lockPath = root + "/" + lockName;
            createLockNode(lockPath);
            //当前线程创建的临时顺序节点
            String clientLockNode = zk.create(lockPath + "/client", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            String preNode = getPreNode(lockPath, clientLockNode);
            // 如果当前节点是最小节点,则表示取得锁
            addLock(clientLockNode);
            if (preNode == null) {
                logger.info("锁获取成功,锁路径为:{}", clientLockNode);
                return true;
            }else{
                unlock(lockName);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return false;
    }
 
    /**
     * 存储本次线程中添加的锁
     * @param lockPath
     */
    private void addLock(String lockPath) {
        List<String> list = nodePathList.get();
        if (list == null) {
            list = new ArrayList<>();
        }
        list.add(lockPath);
        nodePathList.set(list);
    }
 
    /**
     * 删除锁
     */
    public void unlock(String lockName) {
        try {
            String lockPathPrefix = root + "/" + lockName;
            String lockPath = "";
            List<String> list = nodePathList.get();
            if (list != null && list.size() > 0) {
                Iterator<String> iterator = list.iterator();
                while (iterator.hasNext()) {
                    String lockWholePath = iterator.next();
                    if (lockWholePath.contains(lockPathPrefix)) {
                        lockPath = lockWholePath;
                        iterator.remove();
                        break;
                    }
                }
                if (StringUtils.isNotBlank(lockPath)) {
                    Stat stat = zk.exists(lockPath, true);
                    if (stat != null) {
                        zk.delete(lockPath, -1);
                        logger.info("锁释放成功,锁路径为:{}", lockPath);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 优点
  1. 性能较好,可用性高,可以很方便的实现阻塞锁;
  2. 客户端宕机等异常情况下,当前客户端持有的锁可实时释放;
  3. 依据Zookeeper官方API自定义实现,有问题方便排查;
  • 缺点
  1. Zookeeper官方API抛出的各种异常需手动处理;
  2. Zookeeper连接管理,session失效管理需手动处理;
  3. Watch只生效一次,再使用时需重新注册;
  4. 不适用场景:一个线程中先添加A锁再添加B锁,同时另一个线程先添加B锁再添加A锁,该种死锁问题无法解决;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270