使用ZooKeeper实现Java跨JVM的分布式锁

1、使用ZooKeeper实现Java跨JVM的分布式锁java

2、使用ZooKeeper实现Java跨JVM的分布式锁(优化构思)node

3、使用ZooKeeper实现Java跨JVM的分布式锁(读写锁)apache

 

说明:本文是使用Curator框架进行讲解及演示,Curator是对Zookeeper客户端的一个封装,由于Zookeeper的客户端实现偏底层,若是想要实现锁或其余功能都须要本身封装,实现一些简单的功能还能够,若是想要实现锁这种高并发下的东西,不建议本身封装,除非你自信你写的东西比国外大神写的还好~ 若是是研究学习到是能够本身写一下,同时也能够看看开源的代码,那里面仍是有不少值得学习的东西。安全

Zookeeper版本为 Release 3.4.8(stable)服务器

Curator版本为2.9.1session

 

[java]  view plain  copy
 
 在CODE上查看代码片派生到个人代码片
  1. <dependency>  
  2.     <groupId>org.apache.zookeeper</groupId>  
  3.     <artifactId>zookeeper</artifactId>  
  4.     <version>3.4.8</version>  
  5. </dependency>  
  6.   
  7. <dependency>  
  8.     <groupId>org.apache.curator</groupId>  
  9.     <artifactId>curator-recipes</artifactId>  
  10.     <version>2.9.1</version>  
  11. </dependency>  
  12.   
  13. <dependency>  
  14.     <groupId>org.apache.curator</groupId>  
  15.     <artifactId>curator-client</artifactId>  
  16.     <version>2.9.1</version>  
  17. </dependency>  

 

 

锁原理:并发

一、首先要建立一个锁的根节点,好比/mylock。app

 

二、想要获取锁的客户端在锁的根节点下面建立znode,做为/mylock的子节点,节点的类型要选择CreateMode.PERSISTENT_SEQUENTIAL,节点的名字最好用uuid(至于为何用uuid我后面会讲,先说一下~若是不这么作在某种状况下会发生死锁,这一点我看了不少国内朋友本身的实现,都没有考虑到这一层,这也是我为何不建议你们本身去封装这种锁,由于它确实很复杂),假设目前同时有3个客户端想要得到锁,那么/mylock下的目录应该是这个样子的。框架

xxx-lock-0000000001,xxx-lock-0000000002,xxx-lock-0000000003dom

xxx为uuid , 0000000001,0000000002,0000000003 是zook服务端自动生成的自增数字。

 

三、当前客户端经过getChildren(/mylock)获取全部子节点列表并根据自增数字排序,而后判断一下本身建立的节点的顺序是否是在列表当中最小的,若是是 那么获取到锁,若是不是,那么获取本身的前一个节点,并设置监听这个节点的变化,当节点变化时从新执行步骤3 直到本身是编号最小的一个为止。

举例:假设当前客户端建立的节点是0000000002,由于它的编号不是最小的,因此获取不到锁,那么它就找到它前面的一个节点0000000001 并对它设置监听。

 

四、释放锁,当前得到锁的客户端在操做完成后删除本身建立的节点,这样会激发zook的事件给其它客户端知道,这样其它客户端会从新执行(步骤3)。

举例:加入客户端0000000001获取到锁,而后客户端0000000002加入进来获取锁,发现本身不是编号最小的,那么它会监听它前面节点的事件(0000000001的事件)而后执行步骤(3),当客户端0000000001操做完成后删除本身的节点,这时zook服务端会发送事件,这时客户端0000000002会接收到该事件,而后重复步骤3直到获取到锁)

 

上面的步骤实现了一个有序锁,也就是先进入等待锁的客户端在锁可用时先得到锁。

若是想要实现一个随机锁,那么只须要把PERSISTENT_SEQUENTIAL换成一个随机数便可。

 

简单示例:

 

[java]  view plain  copy
 
 在CODE上查看代码片派生到个人代码片
  1. package com.framework.code.demo.zook;  
  2.   
  3. import org.apache.curator.RetryPolicy;  
  4. import org.apache.curator.framework.CuratorFramework;  
  5. import org.apache.curator.framework.CuratorFrameworkFactory;  
  6. import org.apache.curator.framework.recipes.locks.InterProcessMutex;  
  7. import org.apache.curator.retry.ExponentialBackoffRetry;  
  8.   
  9. public class CuratorDemo {  
  10.   
  11.     public static void main(String[] args) throws Exception {  
  12.           
  13.         //操做失败重试机制 1000毫秒间隔 重试3次  
  14.         RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);  
  15.         //建立Curator客户端  
  16.         CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.1.18:2181", retryPolicy);  
  17.         //开始  
  18.         client.start();  
  19.           
  20.         /** 
  21.          * 这个类是线程安全的,一个JVM建立一个就好 
  22.          * mylock 为锁的根目录,咱们能够针对不一样的业务建立不一样的根目录 
  23.          */  
  24.         final InterProcessMutex lock = new InterProcessMutex(client, "/mylock");  
  25.         try {  
  26.             //阻塞方法,获取不到锁线程会挂起。  
  27.             lock.acquire();  
  28.             System.out.println("已经获取到锁");  
  29.              Thread.sleep(10000);  
  30.         } catch (Exception e) {  
  31.             e.printStackTrace();  
  32.         }  
  33.         finally{  
  34.             //释放锁,必需要放到finally里面,已确保上面方法出现异常时也可以释放锁。  
  35.             lock.release();  
  36.         }  
  37.           
  38.         Thread.sleep(10000);  
  39.           
  40.         client.close();  
  41.     }  
  42.   
  43. }  


上面代码再获取锁的地方暂停了10秒钟,咱们使用zook的客户端去查看目录的建立状况,因为我前面已经作了几回测试,因此序号是从12开始的。

 

 

 

模拟多个客户端(也能够认为是多个JVM):

如今把上面的代码改造一下放入到线程中去执行,模拟多个客户端测试

 

[java]  view plain  copy
 
 在CODE上查看代码片派生到个人代码片
  1. public class CuratorDemo {  
  2.   
  3.     public static void main(String[] args) throws Exception {  
  4.         for (int i = 0; i < 10; i++) {  
  5.             //启动10个线程模拟多个客户端  
  6.             Jvmlock jl = new Jvmlock(i);  
  7.             new Thread(jl).start();  
  8.             //这里加上300毫秒是为了让线程按顺序启动,否则有可能4号线程比3号线程先启动了,这样测试就不许了。  
  9.             Thread.sleep(300);  
  10.         }  
  11.     }  
  12.       
  13.     public static class Jvmlock implements Runnable{  
  14.           
  15.         private int num;  
  16.         public Jvmlock(int num) {  
  17.             this.num = num;  
  18.         }  
  19.           
  20.         @Override  
  21.         public void run() {  
  22.             RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);  
  23.             CuratorFramework client = CuratorFrameworkFactory  
  24.                     .newClient("192.168.142.128:2181", retryPolicy);  
  25.             client.start();  
  26.               
  27.             InterProcessMutex lock = new InterProcessMutex(client,  
  28.                     "/mylock");  
  29.             try {  
  30.                 System.out.println("我是第" + num + "号线程,我开始获取锁");  
  31.                 lock.acquire();  
  32.                 System.out.println("我是第" + num + "号线程,我已经获取锁");  
  33.                 Thread.sleep(10000);  
  34.             } catch (Exception e) {  
  35.                 e.printStackTrace();  
  36.             } finally {  
  37.                 try {  
  38.                     lock.release();  
  39.                 } catch (Exception e) {  
  40.                     e.printStackTrace();  
  41.                 }  
  42.             }  
  43.             client.close();  
  44.         }  
  45.     }  
  46.   
  47. }  


经过客户端软件咱们能够看到10个申请锁的节点已经被建立出来了。

 

看一下打印结果,先申请获取锁的线程在锁可用时最早获取到锁,由于他们申请锁时建立节点的顺序号是递增的,先申请锁的客户端建立的节点编号最小,因此先获取到锁

 

[java]  view plain  copy
 
 在CODE上查看代码片派生到个人代码片
  1. 我是第0号线程,我开始获取锁  
  2. 我是第0号线程,我已经获取锁  
  3. 我是第1号线程,我开始获取锁  
  4. 我是第2号线程,我开始获取锁  
  5. 我是第3号线程,我开始获取锁  
  6. 我是第4号线程,我开始获取锁  
  7. 我是第5号线程,我开始获取锁  
  8. 我是第6号线程,我开始获取锁  
  9. 我是第7号线程,我开始获取锁  
  10. 我是第8号线程,我开始获取锁  
  11. 我是第9号线程,我开始获取锁  
  12. 我是第1号线程,我已经获取锁  
  13. 我是第2号线程,我已经获取锁  
  14. 我是第3号线程,我已经获取锁  
  15. 我是第4号线程,我已经获取锁  
  16. 我是第5号线程,我已经获取锁  
  17. 我是第6号线程,我已经获取锁  
  18. 我是第7号线程,我已经获取锁  
  19. 我是第8号线程,我已经获取锁  
  20. 我是第9号线程,我已经获取锁  

 

 

为何节点的名称要加上uuid,这是框架的英文解释。

It turns out there is an edge case that exists when creating sequential-ephemeral nodes. The creation can succeed on the server, but the server can crash before the created node name is returned to the client. However, the ZK session is still valid so the ephemeral node is not deleted. Thus, there is no way for the client to determine what node was created for them. 

Even without sequential-ephemeral, however, the create can succeed on the sever but the client (for various reasons) will not know it. 

Putting the create builder into protection mode works around this. The name of the node that is created is prefixed with a GUID. If node creation fails the normal retry mechanism will occur. On the retry, the parent path is first searched for a node that has the GUID in it. If that node is found, it is assumed to be the lost node that was successfully created on the first try and is returned to the caller.

就是说 当客户端建立了一个节点,这个建立的过程在zook的服务器端已经成功了,可是在将节点的路径返回给客户端以前服务器端挂了, 由于客户端的session仍是有效的,因此这个节点不会删除, 这样客户端就不知道哪一个节点是它建立的。

当客户端发生建立失败的时候,会进行重试,若是这个时候zook已经恢复可用,那么客户端会查询服务器端全部子节点,而后经过和本身建立的uuid对比,若是找到了,说明这个节点是它以前建立的,那么久直接使用它,否则这个节点就会成为一个死节点,致使死锁。

 

实现非公平锁:

重写建立节点的方法,

 

[java]  view plain  copy
 
 在CODE上查看代码片派生到个人代码片
  1. package com.framework.code.demo.zook.lock;  
  2.   
  3. import org.apache.curator.framework.CuratorFramework;  
  4. import org.apache.curator.framework.recipes.locks.StandardLockInternalsDriver;  
  5. import org.apache.zookeeper.CreateMode;  
  6.   
  7. public class NoFairLockDriver extends StandardLockInternalsDriver {  
  8.   
  9.     /** 
  10.      * 随机数的长度 
  11.      */  
  12.     private int numLength;   
  13.     private static int DEFAULT_LENGTH = 5;  
  14.       
  15.     public NoFairLockDriver() {  
  16.         this(DEFAULT_LENGTH);  
  17.     }  
  18.       
  19.     public NoFairLockDriver(int numLength) {  
  20.         this.numLength = numLength;  
  21.     }  
  22.       
  23.     @Override  
  24.     public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception  
  25.     {  
  26.         String newPath = path + getRandomSuffix();  
  27.         String ourPath;  
  28.         if ( lockNodeBytes != null )  
  29.         {  
  30.             //原来使用的是CreateMode.EPHEMERAL_SEQUENTIAL类型的节点  
  31.             //节点名称最终是这样的_c_c8e86826-d3dd-46cc-8432-d91aed763c2e-lock-0000000025  
  32.             //其中0000000025是zook服务器端资自动生成的自增序列 从0000000000开始  
  33.             //因此每一个客户端建立节点的顺序都是按照0,1,2,3这样递增的顺序排列的,因此他们获取锁的顺序与他们进入的顺序是一致的,这也就是所谓的公平锁  
  34.             //如今咱们将有序的编号换成随机的数字,这样在获取锁的时候变成非公平锁了  
  35.             ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL).forPath(newPath, lockNodeBytes);  
  36.             //ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);  
  37.         }  
  38.         else  
  39.         {  
  40.             ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL).forPath(newPath);  
  41.             //ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);  
  42.         }  
  43.         return ourPath;  
  44.     }  
  45.       
  46.     /** 
  47.      * 得到随机数字符串 
  48.      */  
  49.     public String getRandomSuffix() {  
  50.         StringBuilder sb = new StringBuilder();  
  51.         for (int i = 0; i < numLength; i++) {  
  52.             sb.append((int) (Math.random() * 10));  
  53.         }  
  54.         return sb.toString();  
  55.     }  
  56.       
  57. }  



 

把咱们写的类注册进去:

 

[java]  view plain  copy
 
 在CODE上查看代码片派生到个人代码片
  1. InterProcessMutex lock = new InterProcessMutex(client,"/mylock", new NoFairLockDriver());  


仍是上面的例子,在跑一边看结果,能够看到,获取锁的顺序已是无序的了,从而实现了非公平锁。

 

 

[java]  view plain  copy
 
 在CODE上查看代码片派生到个人代码片
    1. 我是第1号线程,我开始获取锁  
    2. 我是第0号线程,我开始获取锁  
    3. 我是第0号线程,我已经获取锁  
    4. 我是第2号线程,我开始获取锁  
    5. 我是第3号线程,我开始获取锁  
    6. 我是第4号线程,我开始获取锁  
    7. 我是第5号线程,我开始获取锁  
    8. 我是第6号线程,我开始获取锁  
    9. 我是第7号线程,我开始获取锁  
    10. 我是第8号线程,我开始获取锁  
    11. 我是第9号线程,我开始获取锁  
    12. 我是第9号线程,我已经获取锁  
    13. 我是第8号线程,我已经获取锁  
    14. 我是第4号线程,我已经获取锁  
    15. 我是第7号线程,我已经获取锁  
    16. 我是第3号线程,我已经获取锁  
    17. 我是第1号线程,我已经获取锁  
    18. 我是第2号线程,我已经获取锁  
    19. 我是第5号线程,我已经获取锁  
    20. 我是第6号线程,我已经获取锁  
相关文章
相关标签/搜索