• ZooKeeper 笔记(6) 分布式锁


      目前分布式锁,比较成熟、主流的方案有基于redis及基于zookeeper的二种方案。

      大体来讲,基于redis的分布式锁核心指令为SETNX,即如果目标key存在,写入缓存失败返回0,反之如果目标key不存在,写入缓存成功返回1,通过区分这二个不同的返回值,可以认为SETNX成功即为获得了锁。

      redis分布式锁,看上去很简单,但其实要考虑周全,并不容易,网上有一篇文章讨论得很详细:http://blog.csdn.net/ugg/article/details/41894947/,有兴趣的可以阅读一下。

      其主要问题在于某些异常情况下,锁的释放会有问题,比如SETNX成功,应用获得锁,这时出于某种原因,比如网络中断,或程序出异常退出,会导致锁无法及时释放,只能依赖于缓存的过期时间,但是过期时间这个值设置多大,也是一个纠结的问题,设置小了,应用处理逻辑很复杂的话,可能会导致锁提前释放,如果设置大了,又会导致锁不能及时释放,所以那篇文章中针对这些细节讨论了很多。

      而基于zk的分布式锁,在锁的释放问题上处理起来要容易一些,其大体思路是利用zk的“临时顺序”节点,需要获取锁时,在某个约定节点下注册一个临时顺序节点,然后将所有临时节点按小从到大排序,如果自己注册的临时节点正好是最小的,表示获得了锁。(zk能保证临时节点序号始终递增,所以如果后面有其它应用也注册了临时节点,序号肯定比获取锁的应用更大)

      当应用处理完成,或者处理过程中出现某种原因,导致与zk断开,超过时间阈值(可配置)后,zk server端会自动删除该临时节点,即:锁被释放。所有参与锁竞争的应用,只要监听父路径的子节点变化即可,有变化时(即:有应用断开或注册时),开始抢锁,抢完了大家都在一边等着,直到有新变化时,开始新一轮抢锁。

      关于zk的分布式锁,网上也有一篇文章写得不错,见http://blog.csdn.net/desilting/article/details/41280869

    个人感觉:zk做分布式锁机制更完善,但zk抗并发的能力弱于redis,性能上略差,建议如果并发要求高,锁竞争激烈,可考虑用redis,如果抢锁的频度不高,用zk更适合。

    最后送福利时间到:

      文中提到的基于zk分布式锁的那篇文章,逻辑上虽然没有问题,但是有些场景下,锁的数量限制可能要求不止1个,比如:某些应用,我希望同时启动2个实例来处理,但是出于HA的考虑,又担心这二个实例会挂掉,这时可以启动4个(或者更多),这些实例中,只允许2个抢到锁的实例可以进行业务处理,其它实例处于standby状态(即:备胎),如果这二个抢到锁的实例挂了(比如异常退出),那么standby的实例会得到锁,即:备胎转正,开始正常业务处理,从而保证了系统的HA。

    对于这些场景,我封装了一个抽象类,大家可在此基础上自行修改:(主要看明白思路就行,代码细节并不重要)

    package cn.cnblogs.yjmyzz.zookeeper;
    
    import org.I0Itec.zkclient.ZkClient;
    import org.apache.commons.collections4.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.Collections;
    import java.util.Date;
    import java.util.List;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    /**
     * Created by yangjunming on 5/27/16.
     * 基于Zookeeper的分布式锁
     */
    public abstract class AbstractLock {
    
        private int lockNumber = 1; //允许获取的锁数量(默认为1,即最小节点=自身时,认为获得锁)
        private ZkClient zk = null;
        private String rootNode = "/lock"; //根节点名称
        private String selfNode;
        private final String className = this.getClass().getSimpleName(); //当前实例的className
        private String selfNodeName;//自身注册的临时节点名
        private boolean handling = false;
        protected final Logger logger = LoggerFactory.getLogger(this.getClass());
        private static final JsonUtil jsonUtil = new FastJsonUtil();
        private static final String SPLIT = "/";
        private String selfNodeFullName;
    
    
        /**
         * 通过Zk获取分布式锁
         */
        protected void getLock(int lockNumber) {
            setLockNumber(lockNumber);
            initBean();
            initNode();
            subscribe();
            register();
            heartBeat();
            remainRunning();
        }
    
    
        protected void getLock() {
            getLock(1);
        }
    
    
        /**
         * 初始化结点
         */
        private void initNode() {
    
            String error;
            if (!rootNode.startsWith(SPLIT)) {
                error = "rootNode必须以" + SPLIT + "开头";
                logger.error(error);
                throw new RuntimeException(error);
            }
    
            if (rootNode.endsWith(SPLIT)) {
                error = "不能以" + SPLIT + "结尾";
                logger.error(error);
                throw new RuntimeException(error);
            }
    
            int start = 1;
            int index = rootNode.indexOf(SPLIT, start);
            String path;
            while (index != -1) {
                path = rootNode.substring(0, index);
                if (!zk.exists(path)) {
                    zk.createPersistent(path);
                }
                start = index + 1;
                if (start >= rootNode.length()) {
                    break;
                }
                index = rootNode.indexOf(SPLIT, start);
            }
    
            if (start < rootNode.length()) {
                if (!zk.exists(rootNode)) {
                    zk.createPersistent(rootNode);
                }
            }
    
            selfNode = rootNode + SPLIT + className;
    
            if (!zk.exists(selfNode)) {
                zk.createPersistent(selfNode);
            }
        }
    
        /**
         * 向zk注册自身节点
         */
        private void register() {
            selfNodeName = zk.createEphemeralSequential(selfNode + SPLIT, StringUtils.EMPTY);
            if (!StringUtils.isEmpty(selfNodeName)) {
                selfNodeFullName = selfNodeName;
                logger.info("自身节点:" + selfNodeName + ",注册成功!");
                selfNodeName = selfNodeName.substring(selfNode.length() + 1);
            }
            checkMin();
        }
    
        /**
         * 订阅zk的节点变化
         */
        private void subscribe() {
            zk.subscribeChildChanges(selfNode, (parentPath, currentChilds) -> {
                checkMin();
            });
        }
    
        /**
         * 检测是否获得锁
         */
        private void checkMin() {
            List<String> list = zk.getChildren(selfNode);
            if (CollectionUtils.isEmpty(list)) {
                logger.error(selfNode + " 无任何子节点!");
                lockFail();
                handling = false;
                return;
            }
            //按序号从小到大排
            Collections.sort(list);
    
            //如果自身ID在前N个锁中,则认为获取成功
            int max = Math.min(getLockNumber(), list.size());
            for (int i = 0; i < max; i++) {
                if (list.get(i).equals(selfNodeName)) {
                    if (!handling) {
                        lockSuccess();
                        handling = true;
                        logger.info("获得锁成功!");
                    }
                    return;
                }
            }
    
            int selfIndex = list.indexOf(selfNodeName);
            if (selfIndex > 0) {
                logger.info("前面还有节点" + list.get(selfIndex - 1) + ",获取锁失败!");
            } else {
                logger.info("获取锁失败!");
            }
            lockFail();
    
            handling = false;
        }
    
        /**
         * 获得锁成功的处理回调
         */
        protected abstract void lockSuccess();
    
        /**
         * 获得锁失败的处理回调
         */
        protected abstract void lockFail();
    
        /**
         * 初始化相关的Bean对象
         */
        protected abstract void initBean();
    
    
        protected void setZkClient(ZkClient zk) {
            this.zk = zk;
        }
    
        protected int getLockNumber() {
            return lockNumber;
        }
    
        protected void setLockNumber(int lockNumber) {
            this.lockNumber = lockNumber;
        }
    
        protected void setRootNode(String value) {
            this.rootNode = value;
        }
    
        /**
         * 防程序退出
         */
        private void remainRunning() {
            byte[] lock = new byte[0];
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    logger.error("remainRunning出错:", e);
                }
            }
        }
    
        /**
         * 定时向zk发送心跳
         */
        private void heartBeat() {
            ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
            service.scheduleAtFixedRate(() -> {
                HeartBeat heartBeat = new HeartBeat();
                heartBeat.setHostIp(NetworkUtil.getHostAddress());
                heartBeat.setHostName(NetworkUtil.getHostName());
                heartBeat.setLastTime(new Date());
                heartBeat.setPid(RuntimeUtil.getPid());
                zk.writeData(selfNodeFullName, jsonUtil.toJson(heartBeat));
            }, 0, 15, TimeUnit.SECONDS);
        }
    }
    

    这个类中,提供了三个抽象方法:

        /**
         * 获得锁成功的处理回调
         */
        protected abstract void lockSuccess();
    
        /**
         * 获得锁失败的处理回调
         */
        protected abstract void lockFail();
    
        /**
         * 初始化相关的Bean对象
         */
        protected abstract void initBean();
    

    用于处理抢锁成功、抢锁失败、及开抢前的一些对象初始化处理,子类继承后,只要实现这3个具体的方法即可,同时该抽象类默认还提供了心跳机制,用于定时向zk汇报自身的健康状态。

  • 相关阅读:
    最短路问题
    树的最小支配集,最小点覆盖与最大独立集
    最近公共祖先(LCA)
    图的生成树
    图的遍历
    图的存储结构
    博客园总算支持Markdown了
    关于VMware(虚拟机) 出现错误时处理办法
    Docker 部署 _实现每日情话 定时推送(apscheduler)
    tkinter + 爬虫 实现影视在线资源系统
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/distributed-lock-using-zookeeper.html
Copyright © 2020-2023  润新知