还没搞明白分布式锁?我来用最通俗的方式来讲清楚—zk篇

1,451 阅读5分钟

前言

搬起电脑,拿起键盘,回想上一次写文章,好像已经想不起来啥时候写的了。

并不是因为偷懒啊,因为在准备面试,而后又进行了一波找工作之旅,也算是收获还不错,面了6家收到了5个offer,然后敲定了工作。不过还没有去面各种大厂,因为感觉目前的技术还达不到大厂的要求,而且算法这块也比较烂。但这次也算是找到了一个挺不错的公司,进大厂就当做是下一阶段的小目标吧。

回到正题,来看分布式锁,在上一篇中也已经介绍了何为分布式锁,也说了基于redis实现的分布式锁,这一篇就来介绍基于zookeeper来实现分布式锁。

zookeeper实现

很多小伙伴都知道在分布式系统中,可以用zk来做注册中心,但其实在除了做祖册中心以外,用zk来做分布式锁也是很常见的一种方案。

先来看一下zk中是如何创建一个节点的

zk中存在create [-s] [-e] path [data]命令,-s为创建有序节点,-e创建临时节点。

这样就创建了一个父节点并为父节点创建了一个子节点,组合命令意为创建一个临时的有序节点。而zk中分布式锁主要就是靠创建临时的顺序节点来实现的。至于为什么要用顺序节点和为什么用临时节点不用持久节点?先考虑一下,下文将作出说明。

同时还有zk中如何查看节点

zk中ls [-w] path为查看节点命令,-w为添加一个watch(监视器),/为查看根节点所有节点,可以看到我们刚才所创建的节点,同时如果是跟着指定节点名字的话为查看指定节点下的子节点。

后面的00000000为zk为顺序节点增加的顺序。注册监听器也是zk实现分布式锁中比较重要的一个东西,下面来看一下zk实现分布式锁的主要流程。

1、当第一个线程进来时会去父节点上创建一个临时的顺序节点
2、第二个线程进来发现锁已经被持有了,就会为当前持有锁的节点注册一个watcher监听器
3、第三个线程进来发现锁已经被持有了,因为是顺序节点的缘故,就会为上一个节点去创建一个watcher监听器
4、当第一个线程释放锁后,删除节点,由它的下一个节点去占有锁

看到这里,聪明的小伙伴们都已经看出来顺序节点的好处了。非顺序节点的话,每进来一个线程进来都会去持有锁的节点上注册一个监听器,容易引发“羊群效应”。

这么大一群羊一起向你飞奔而来,不管你顶不顶得住,反正zk服务器是会增大宕机的风险。而顺序节点的话就不会,顺序节点当发现已经有线程持有锁后,会向它的上一个节点注册一个监听器,这样当持有锁的节点释放后,也只有持有锁的下一个节点可以抢到锁,相当于是排好队来执行的,降低服务器宕机风险。

至于为什么使用临时节点,和redis的过期时间一个道理,就算zk服务器宕机,临时节点会随着服务器的宕机而消失,避免了死锁的情况。

下面来上一段代码的实现

public class ZooKeeperDistributedLock implements Watcher {

    private ZooKeeper zk;
    private String locksRoot = "/locks";
    private String productId;
    private String waitNode;
    private String lockNode;
    private CountDownLatch latch;
    private CountDownLatch connectedLatch = new CountDownLatch(1);
    private int sessionTimeout = 30000;

    public ZooKeeperDistributedLock(String productId) {
        this.productId = productId;
        try {
            String address = "192.168.189.131:2181,192.168.189.132:2181";
            zk = new ZooKeeper(address, sessionTimeout, this);
            connectedLatch.await();
        } catch (IOException e) {
            throw new LockException(e);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }

    public void process(WatchedEvent event) {
        if (event.getState() == KeeperState.SyncConnected) {
            connectedLatch.countDown();
            return;
        }

        if (this.latch != null) {
            this.latch.countDown();
        }
    }

    public void acquireDistributedLock() {
        try {
            if (this.tryLock()) {
                return;
            } else {
                waitForLock(waitNode, sessionTimeout);
            }
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }
    //获取锁
    public boolean tryLock() {
        try {
        // 传入进去的locksRoot + “/” + productId
        // 假设productId代表了一个商品id,比如说1
        // locksRoot = locks
        // /locks/10000000000,/locks/10000000001,/locks/10000000002
        lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

        // 看看刚创建的节点是不是最小的节点
        // locks:10000000000,10000000001,10000000002
        List<String> locks = zk.getChildren(locksRoot, false);
        Collections.sort(locks);

        if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
            //如果是最小的节点,则表示取得锁
            return true;
        }

        //如果不是最小的节点,找到比自己小1的节点
	  int previousLockIndex = -1;
            for(int i = 0; i < locks.size(); i++) {
		if(lockNode.equals(locksRoot + “/” + locks.get(i))) {
	         	    previousLockIndex = i - 1;
		    break;
		}
	   }
	   
	   this.waitNode = locks.get(previousLockIndex);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
        return false;
    }

    private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
        Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
        if (stat != null) {
            this.latch = new CountDownLatch(1);
            this.latch.await(waitTime, TimeUnit.MILLISECONDS);
            this.latch = null;
        }
        return true;
    }

    //释放锁
    public void unlock() {
        try {
            System.out.println("unlock " + lockNode);
            zk.delete(lockNode, -1);
            lockNode = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }
    //异常
    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public LockException(String e) {
            super(e);
        }

        public LockException(Exception e) {
            super(e);
        }
    }
}

小结

既然明白了redis和zk分别对分布式锁的实现,那么总该有所不同的吧。没错,我都帮大家整理好了。

1、实现方式的不同,redis实现为去插入一条占位数据,而zk实现为去注册一个临时节点。
2、遇到宕机情况时,redis需要等到过期时间到了后自动释放锁,而zk因为是临时节点,在宕机时候已经是删除了节点去释放锁。
3、redis在没抢占到锁的情况下一般会去自旋获取锁,比较浪费性能,而zk是通过注册监听器的方式获取锁,性能而言优于redis。

不过具体要采用哪种实现方式,还是需要具体情况具体分析,结合项目引用的技术栈来落地实现。

完!

都看到着了,点个赞点个关注再走呗~