在分布式解决方案中,Zookeeper是一个分布式协调工具。当多个JVM客户端,同时在ZooKeeper上建立相同的一个临时节点,由于临时节点路径是保证惟一,只要谁可以建立节点成功,谁就可以获取到锁。没有建立成功节点,就会进行等待,当释放锁的时候,采用事件通知给客户端从新获取锁资源。若是请求超时直接返回给客户端超时,从新请求便可。sql
为了更好的展示效果,我这里设置每一个线程请求须要1s,请求超时时间为30s。数据库
首先咱们先写一个测试类,模拟多线程多客户端请求的状况:缓存
public class ZkLockTest implements Runnable { private ZkLock zkLock = new ZkDistributedLock(); public void run() { try { if (zkLock.getLock((long)30000,null)) { System.out.println("线程:" + Thread.currentThread().getName() + ",抢购成功:" + System.currentTimeMillis()); } else { System.out.println("线程:" + Thread.currentThread().getName() + ",抢购超时失败请重试:" + System.currentTimeMillis()); } Thread.sleep(1000); } catch (Exception e) { } finally { zkLock.unLock(); } } public static void main(String[] args) { System.out.println("zk分布式锁开始。。"); for (int i = 0; i < 100; i++) { new Thread(new ZkLockTest()).start(); } } }
模拟100个线程,去同时争夺锁。固然上述写法 100个线程不会同时启动,若是须要的话能够用信号量的形式控制。网络
其次,写一个锁的接口多线程
public interface ZkLock { // 获取锁 Boolean getLock(Long acquireTimeout,Long endTime); // 释放锁 void unLock(); }
这里我定义了两个接口,分别对应获取锁和释放锁。架构
在获取锁中有两个参数,含义分别为锁超时时间和最终计算的超时时间,具体看下文代码就懂了。并发
public class ZkDistributedLock implements ZkLock { // 集群链接地址 private String CONNECTION = "127.0.0.1:2181"; // zk客户端链接 private ZkClient zkClient = new ZkClient(CONNECTION); // path路径 private String lockPath = "/lock"; private CountDownLatch countDownLatch; //请求设置的超时时间:acquireTimeout 毫秒。最终超时时间endTime public Boolean getLock(Long acquireTimeout,Long endTime) { Boolean lock = false; if (endTime == null) { //等待超时时间 endTime = System.currentTimeMillis() + acquireTimeout; } if (tryLock()) { System.out.println("####获取锁成功######"); lock = true; } else { if (waitLock(endTime)) { if (getLock(null,endTime)) { lock = true; } } } return lock; } public void unLock() { if (zkClient != null) { System.out.println("#######释放锁#########"); zkClient.close(); } } private boolean tryLock() { try { zkClient.createEphemeral(lockPath); return true; } catch (Exception e) { return false; } } private Boolean waitLock(Long endTime) { // System.out.println("进入等待"); // 使用zk临时事件监听 IZkDataListener iZkDataListener = null; try { // 使用zk临时事件监听 iZkDataListener = new IZkDataListener() { public void handleDataDeleted(String path) throws Exception { if (countDownLatch != null) { countDownLatch.countDown(); } } public void handleDataChange(String arg0, Object arg1) throws Exception { } }; // 注册事件通知 zkClient.subscribeDataChanges(lockPath, iZkDataListener); if (System.currentTimeMillis() < endTime) { if (zkClient.exists(lockPath)) { countDownLatch = new CountDownLatch(1); try { countDownLatch.await(); return true; } catch (Exception e) { } } else { return true; } } else { System.out.println("超时返回"); } } catch (Exception e) { } finally { // 监听完毕后,移除事件通知 zkClient.unsubscribeDataChanges(lockPath, iZkDataListener); } return false; } }
这个类是我实现zk锁的核心类,和上文原理图中相似。首先用户请求的时候须要获取锁,第一个争夺到锁的用户执行相关逻辑后释放锁,在这个过程当中若是程序出错断开链接,由于临时节点的缘故,节点也会自动删除释放锁的。nosql
另外就是其余争夺锁失败的用户,我这里设置了必定的等待时间,当在时间内原锁释放,仍是能够从新去获取锁的。这里要说下锁释放的监听,在原生的zookeeper中,使用watcher须要每次先注册,并且使用一次就须要注册一次。而在zkClient中,没有注册watcher的必要,而是引入了listener的概念,即只要client在某一个节点中注册了listener,只要服务端发生变化,就会通知当前注册listener的客户端。我这里使用的是IZkDataListener,这个类是zkClient提供的一个接口,它能够在当前节点数据内容或版本发生变化或者当前节点被删除时触发。分布式
触发后咱们就能够从新去争夺锁,当再次争夺失败进入等待时会再次检测当前请求是否超时。高并发
下面咱们来看下上述代码的实现效果:
zk分布式锁开始。。 ####获取锁成功###### 线程:Thread-3,抢购成功:1544183770509 #######释放锁######### ####获取锁成功###### 线程:Thread-81,抢购成功:1544183771555 #######释放锁######### ......... 超时返回 线程:Thread-11,抢购超时失败请重试:1544183800677 超时返回 线程:Thread-1,抢购超时失败请重试:1544183800681 #######释放锁######### #######释放锁######### ####获取锁成功###### 线程:Thread-49,抢购成功:1544183801710 超时返回 线程:Thread-25,抢购超时失败请重试:1544183801729 超时返回 #######释放锁######### #######释放锁#########
释放锁说的可能并不许确,应该说是关闭链接,有些线程其实是没有获得锁的。
简单尝试了下zk实现分布式锁的方式,固然上述代码若是应用到生产中确定问题仍是很多的,由于兴趣点不在这,就不仔细研究了。简单来讲,相比其余方式实现步骤更为复杂,感受更容易出问题。
通过三种方式的应用和简单实践,总结实现分布式锁三种方式的优缺点以下
一、数据库实现:
优势,实现简单只是for update的显示加锁。缺点,性能问题较大,并且自己系统在设计时是须要尽可能减轻数据库的压力的。
二、Redis实现:
优势:通常互联网项目都会集成,自己是nosql数据库,缓存实现简单,高并发应付自如,同时新版的Jedis完美解决了以往程序出错,未设置超时时间死锁的问题。
缺点:网络问题可能会引发锁删除失败,超时时间有必定的延迟。
三、ZooKeeper实现:
优势:Zookeeper临时节点先天可控的有效期设置,避免了程序引起的死锁问题
缺点:实现过于繁杂,相比其余两种写法更容易出问题,另外还须要单独维护zk。
我我的更为推荐Redis的实现方式,实现简单,性能也比较好,同时引入集群能够提升可用性。Jedis多参的设置方式也较好的保证了有效期的控制和死锁的问题
欢迎工做一到五年的Java工程师朋友们加入Java架构开发: 855835163 群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用本身每一分每一秒的时间来学习提高本身,不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!