前言
前面写了一篇关于zookeeper入门详解的文章,如果有不了解的可以回看!!
迄今为止最易懂Zookeeper入门详解!
这篇文章再和大家聊一下zookeeper在实际场景下应用:分布式锁!!
锁,我想不需要过多的解释,大家都知道怎么一回事吧?如果阅读量破百,再肝一篇关于锁的详解!
在多线程的环境下,由于上下文切换,数据可能会出现不一致的情况或者数据被污染,这时候需要保证数据的安全,就需要使用到锁。所谓的锁机制就是,当一个线程需要修改这个数据的时候进行保护,其他的线程不可以修改,只有当前线程读取完了,其他线程才可以使用。
在单机的情况下,可能synchronized关键字或者Lock类可以解决多线程环境下数据安全的问题,但是在分布式集群部署的情况下并不能保证!这就需要分布式锁的介入!
本文主要介绍使用zookeeper实现分布式锁,后序文章会继续讲解(redis,mysql)
正文
正常的线程同步的机制有哪些?
- 互斥:保证同一时间只有一个线程可以操作共享资源 synchronized,Lock等
- 信号量:多个任务同时访问,同时限制数量,比如发令枪CountDownLatch,CyclicBarrier,Semaphore等。
分布式锁的实现主要有哪些呢?
- 分布式锁实现主要以Zookeeper、Redis、MySQL这三种为主
下面主要聊聊Zookeeper实现分布式锁
zookeeper客户端选型:
- 原生zookeeper客户端,有watcher一次性、无超时重连机制等一系列问题。
- ZkClient,解决了原生客户端一些问题,一些存量老系统中还在使用
- curator,提供了各种应用场景(封装了分布式锁,计数器等),新项目首选
zookeeper分布式锁的实现原理:
在zookeeper中规定,在同一个时刻,不可以多个客户端创建同一个节点,可以利用这个特性去实现分布式锁。我们首选使用zookeeper的临时节点,因为zk的临时节点会在session结束的时候摧毁。watcher机制,在代表锁资源的节点被删除,即可以触发watcher解除阻塞重新去获取锁,这也是zookeeper分布式锁较其他分布式锁方案的一大优势。
?:为什么不适用持久化节点?
废话不多说!直接上代码!!
基于临时节点的方案
第一种方案实现较为简单,逻辑就是谁创建成功该节点,谁就持有锁,创建失败的自己进行阻塞,A线程先持有锁,B线程获取失败就会阻塞,同时对/lockPath设置监听,A线程执行完操作后删除节点,触发监听器,B线程此时解除阻塞,重新去获取锁。
下面使用Java代码的形式实现!!!
- 创建MyLock接口
public interface MyLock { //等待锁 public void waitLock(); //加锁 public void lock(); //尝试去获取锁 public boolean tryLock(); //释放锁 public void unlock();}
- 编写实现类ZookeeperLock
public class ZookeeperLock implements MyLock { private static final String IP_PORT = "127.0.0.1:2181"; private static final String Z_NODE = "/LOCK_01"; private ZkClient zkClient; private CountDownLatch countDownLatch = new CountDownLatch(1); ZookeeperLock() { zkClient = new ZkClient(IP_PORT); } public void waitLock() { IZkDataListener iZkDataListener = new IZkDataListener() { public void handleDataChange(String s, Object o) throws Exception { } public void handleDataDeleted(String s) throws Exception { System.out.println("释放锁"); countDownLatch.countDown(); } }; zkClient.subscribeDataChanges(Z_NODE, iZkDataListener); if (zkClient.exists(Z_NODE)) { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } zkClient.unsubscribeDataChanges(Z_NODE, iZkDataListener); } public void lock() { if (tryLock()) { System.out.println(Thread.currentThread().getName() + "获取到锁"); return; } //等待锁 waitLock(); //重新尝试获取锁 lock(); } public boolean tryLock() { try { zkClient.createPersistent(Z_NODE); return true; } catch (ZkNodeExistsException e) { return false; } } public void unlock() { zkClient.delete(Z_NODE); System.out.println("释放锁"); }}
- 编写测试类
public class ZkTest { static int inv = 5; private static final int NUM = 10; public static void main(String[] args) { for (int i = 0; i < NUM; i++) { new Thread(new Runnable() { ZookeeperLock zookeeperLock = null; public void run() { try { zookeeperLock = new ZookeeperLock(); zookeeperLock.lock(); Thread.sleep(1000); if (inv > 0) { inv--; } System.out.println(inv); } catch (Exception e) { } finally { zookeeperLock.unlock(); } } }).start(); } }}
缺点:
线程每次去竞争锁,都只会有一个线程拿到锁,当线程数过于大的时候会发生“惊群”现象,zookeeper节点可能会运行缓慢甚至宕机。这是因为其他线程没获取到锁时都会监听/lockPath节点,当A线程释放完毕,海量的线程都同时停止阻塞,去争抢锁,这种操作十分耗费资源。
基于临时顺序节点方案
临时顺序节点与临时节点不一样,临时顺序节点生成的节点是有顺序的,可以利用有顺序这个特点,只要当前线程去监听上一个序号的节点,每次去获取锁的时候都判断自己的序号是否最小,如果是最小就获取到锁,执行完毕释放锁,删除节点。
具体的代码流程:
基于临时顺序节点的代码实现(并实现了可重入锁)!!并使用CountDownLatch作为发令枪,当为0的时候唤醒线程获取锁!
- zookeeper初始化
public class Zk implements Lock { private CountDownLatch cdl = new CountDownLatch(1); private static final String IP_PORT = "127.0.0.1:2181"; private static final String Z_NODE = "/LOCK"; private volatile String beforePath; private volatile String path; private AtomicInteger atomic = new AtomicInteger(0); private static ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap(); private ZkClient zkClient = new ZkClient(IP_PORT); public Zk() { if (!zkClient.exists(Z_NODE)) { zkClient.createPersistent(Z_NODE); } } }
- 加锁的方法
public void lock() { if (tryLock()) { System.out.println("获得锁"); } else { // 进入等待 监听 waitForLock(); } }
- 尝试去加锁
public synchronized boolean tryLock() { //可重入锁// if(atomic.get()>0){// atomic.incrementAndGet();// return true;// } //可重入锁 if (concurrentHashMap.contains(Thread.currentThread())) { Integer i = concurrentHashMap.get(Thread.currentThread()); if (i > 0) { concurrentHashMap.replace(Thread.currentThread(), i, i + 1); return true; } } // 第一次创建自己的临时节点 if (path==null) { path = zkClient.createEphemeralSequential(Z_NODE + "/", "lock"); } // 对节点排序 List children = zkClient.getChildren(Z_NODE); Collections.sort(children); // 当前的是最小节点就返回加锁成功 if (path.equals(Z_NODE + "/" + children.get(0))) { //可重入 //atomic.incrementAndGet(); concurrentHashMap.put(Thread.currentThread(), 1); return true; } else { // 不是最小节点 就找到自己的前一个 依次类推 释放也是一样 int i = Collections.binarySearch(children, path.substring(Z_NODE.length() + 1)); beforePath = Z_NODE + "/" + children.get(i - 1); } return false; }
- 等待并监听
public void waitForLock() { IZkDataListener listener = new IZkDataListener() { public void handleDataChange(String s, Object o) throws Exception { } public void handleDataDeleted(String s) throws Exception { System.out.println(Thread.currentThread().getName() + ":监听到节点删除"); //当为0的时候唤醒线程继续执行 cdl.countDown(); } }; // 监听 this.zkClient.subscribeDataChanges(beforePath, listener); if (zkClient.exists(beforePath)) { try { System.out.println("加锁失败等待"); //等待,线程挂起 cdl.await(); } catch (InterruptedException e) { e.printStackTrace(); } } // 释放监听 zkClient.unsubscribeDataChanges(beforePath, listener); // 再次尝试 lock(); }
- 释放锁
public void unlock() { //先判断可重入// if(atomic.get()>1){// atomic.decrementAndGet();// return;// }else {// atomic.set(0);// } Integer i = concurrentHashMap.get(Thread.currentThread()); if (i > 1) { concurrentHashMap.replace(Thread.currentThread(), i, i - 1); return; } else { concurrentHashMap.replace(Thread.currentThread(), i, 0); } zkClient.delete(path); }
:代码分别使用AtomicInteger和ConcurrentHashMap实现了可重入锁!!大家选其一使用即可!
Curator分布式锁工具
Apache Curator是一个高级的包装类库和框架,使得ZooKeeper非常简单易用。Patrixck Hunt(Zookeeper)以一句“Guava is to Java that Curator to Zookeeper”给Curator予高度评价。
- 初始化
public class ZkLockForCurator { private static final String IP_PORT = "127.0.0.1:2181"; private static final String Z_NODE = "/LOCK"; private CuratorFramework client; private InterProcessMutex lock; ZkLockForCurator(){ //RetryPolicy为重试策略, //第一个参数为baseSleepTimeMs初始的sleep时间 //第二个参数为maxRetries,最大重试次数 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); client = CuratorFrameworkFactory.newClient(IP_PORT,retryPolicy); lock = new InterProcessMutex(client,Z_NODE); } public void lock() throws Exception { lock.acquire(10, TimeUnit.SECONDS); } public void unLock() throws Exception { lock.release(); }}
源码以及测试类地址:
https://github.com/yekai1/zookeeperDemo
总结:
zk通过临时节点解决了死锁的问题,一旦客户端失去链接或者突然宕机,也就是session断开,节点就会删除,其他客户端重新去获取锁,利用临时顺序节点,防止了“羊群效应”,zookeeper集群只要有半数以上就可以提供服务!