前言

       前面写了一篇关于zookeeper入门详解的文章,如果有不了解的可以回看!!

     迄今为止最易懂Zookeeper入门详解!

这篇文章再和大家聊一下zookeeper在实际场景下应用:分布式锁!!

        锁,我想不需要过多的解释,大家都知道怎么一回事吧?如果阅读量破百,再肝一篇关于锁的详解!

在多线程的环境下,由于上下文切换,数据可能会出现不一致的情况或者数据被污染,这时候需要保证数据的安全,就需要使用到锁。所谓的锁机制就是,当一个线程需要修改这个数据的时候进行保护,其他的线程不可以修改,只有当前线程读取完了,其他线程才可以使用。

在单机的情况下,可能synchronized关键字或者Lock类可以解决多线程环境下数据安全的问题,但是在分布式集群部署的情况下并不能保证!这就需要分布式锁的介入!

    本文主要介绍使用zookeeper实现分布式锁,后序文章会继续讲解(redis,mysql)

正文

 正常的线程同步的机制有哪些?
  • 互斥:保证同一时间只有一个线程可以操作共享资源 synchronizedLock
  • 信号量:多个任务同时访问,同时限制数量,比如发令枪CountDownLatchCyclicBarrierSemaphore等。
分布式锁的实现主要有哪些呢?
  • 分布式锁实现主要以ZookeeperRedisMySQL这三种为主
下面主要聊聊Zookeeper实现分布式锁
zookeeper客户端选型:
  • 原生zookeeper客户端,有watcher一次性、无超时重连机制等一系列问题。
  • ZkClient,解决了原生客户端一些问题,一些存量老系统中还在使用
  • curator,提供了各种应用场景(封装了分布式锁,计数器等),新项目首选
zookeeper分布式锁的实现原理:

    在zookeeper中规定,在同一个时刻,不可以多个客户端创建同一个节点,可以利用这个特性去实现分布式锁。我们首选使用zookeeper的临时节点,因为zk的临时节点会在session结束的时候摧毁。watcher机制,在代表锁资源的节点被删除,即可以触发watcher解除阻塞重新去获取锁,这也是zookeeper分布式锁较其他分布式锁方案的一大优势。

?:为什么不适用持久化节点?

废话不多说!直接上代码!!

基于临时节点的方案

        第一种方案实现较为简单,逻辑就是谁创建成功该节点,谁就持有锁,创建失败的自己进行阻塞,A线程先持有锁,B线程获取失败就会阻塞,同时对/lockPath设置监听,A线程执行完操作后删除节点,触发监听器,B线程此时解除阻塞,重新去获取锁。

zookeeper atc密码网了 zookeeper 锁_zookeeper分布式锁

下面使用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线程释放完毕,海量的线程都同时停止阻塞,去争抢锁,这种操作十分耗费资源。

基于临时顺序节点方案

        临时顺序节点与临时节点不一样,临时顺序节点生成的节点是有顺序的,可以利用有顺序这个特点,只要当前线程去监听上一个序号的节点,每次去获取锁的时候都判断自己的序号是否最小,如果是最小就获取到锁,执行完毕释放锁,删除节点。

zookeeper atc密码网了 zookeeper 锁_分布式锁_02

具体的代码流程:

zookeeper atc密码网了 zookeeper 锁_zookeeper_03

       基于临时顺序节点的代码实现(并实现了可重入锁)!!并使用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);    }

zookeeper atc密码网了 zookeeper 锁_分布式锁_04

:代码分别使用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集群只要有半数以上就可以提供服务