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

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

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

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

Zookeeper版本为 Release 3.4.8(stable)

Curator版本为2.9.1

  1. <dependency>
  2. <groupId>org.apache.zookeeper</groupId>
  3. <artifactId>zookeeper</artifactId>
  4. <version>3.4.8</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.curator</groupId>
  8. <artifactId>curator-recipes</artifactId>
  9. <version>2.9.1</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.apache.curator</groupId>
  13. <artifactId>curator-client</artifactId>
  14. <version>2.9.1</version>
  15. </dependency>

锁原理:

1、首先要创建一个锁的根节点,比如/mylock。

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

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

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

3、当前客户端通过getChildren(/mylock)获取所有子节点列表并根据自增数字排序,然后判断一下自己创建的节点的顺序是不是在列表当中最小的,如果是 那么获取到锁,如果不是,那么获取自己的前一个节点,并设置监听这个节点的变化,当节点变化时重新执行步骤3 直到自己是编号最小的一个为止。

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

4、释放锁,当前获得锁的客户端在操作完成后删除自己创建的节点,这样会激发zook的事件给其它客户端知道,这样其它客户端会重新执行(步骤3)。

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

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

如果想要实现一个随机锁,那么只需要把PERSISTENT_SEQUENTIAL换成一个随机数即可。

简单示例:

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

上面代码再获取锁的地方暂停了10秒钟,我们使用zook的客户端去查看目录的创建情况,由于我前面已经做了几次测试,所以序号是从12开始的。

模拟多个客户端(也可以认为是多个JVM):

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

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

通过客户端软件我们可以看到10个申请锁的节点已经被创建出来了。

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

  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对比,如果找到了,说明这个节点是它之前创建的,那么久直接使用它,不然这个节点就会成为一个死节点,导致死锁。

实现非公平锁:

重写创建节点的方法,

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

把我们写的类注册进去:

  1. InterProcessMutex lock = new InterProcessMutex(client,"/mylock", new NoFairLockDriver());

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

    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号线程,我已经获取锁

使用ZooKeeper实现Java跨JVM的分布式锁的更多相关文章

  1. 使用ZooKeeper实现Java跨JVM的分布式锁(读写锁)

    一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 读写 ...

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

    一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 说明 ...

  3. Java使用Redis实现分布式锁来防止重复提交问题

    如何用消息系统避免分布式事务? - 少年阿宾 - BlogJavahttp://www.blogjava.net/stevenjohn/archive/2018/01/04/433004.html [ ...

  4. zookeeper笔记之基于zk实现分布式锁

    一.分布式锁概述 Java中基于AQS框架提供了一系列的锁,但是当需要在集群中的多台机器上互斥执行一段代码或使用资源时Java提供的这种单机锁就没了用武之地,此时需要使用分布式锁协调它们.分布式锁有很 ...

  5. ZooKeeper(八)-- Curator实现分布式锁

    1.pom.xml <dependencies> <dependency> <groupId>junit</groupId> <artifactI ...

  6. Java基于redis实现分布式锁(SpringBoot)

    前言 分布式锁,其实原理是就是多台机器,去争抢一个资源,谁争抢成功,那么谁就持有了这把锁,然后去执行后续的业务逻辑,执行完毕后,把锁释放掉. 可以通过多种途径实现分布式锁,例如利用数据库(mysql等 ...

  7. java中redis的分布式锁工具类

    使用方式 try { if(PublicLock.getLock(lockKey)){ //这里写代码逻辑,执行完后需要释放锁 PublicLock.freeLock(lockKey); } } ca ...

  8. java基础之----redi分布式锁

    最近项目中,用到了redis分布式锁,使用过程有些心得,所以希望分享给大家. 首先我们意识里要知道分布锁有哪些? 分布式锁一般分三种,基于数据库的乐观锁,基于redis的分布式锁,基于zookeper ...

  9. java基于mongodb实现分布式锁

    原理 通过线程安全findAndModify 实现锁 实现 定义锁存储对象: /** * mongodb 分布式锁 */ @Data @NoArgsConstructor @AllArgsConstr ...

随机推荐

  1. Python WSGI v1.0 中文版(转)

    add by zhj: WSGI全称Web Server Gateway Interface,即Web网关接口.其实它并不是OSI七层协议中的协议,它就是一个接口而已,即函数,而WSGI规定了该接口的 ...

  2. Python(数据库之约束表的关系)

    一.约束 约束条件与数据类型的宽度一样,都是可选参数 作用:用于保证数据的完整性和一致性 主要分为: RIMARY KEY (PK) 标识该字段为该表的主键,可以唯一的标识记录 FOREIGN KEY ...

  3. Tomcat WEB站点部署

    上线的代码有两种方式, 第一种方式是直接将程序目录放在webapps目录下面 第二种方式是使用开发工具将程序打包成war包,然后上传到webapps目录下面.下面让我们见识一下这种方式 这个网站里面已 ...

  4. Mysql学习笔记—视图

    1.什么是视图 视图(View)是一种虚拟存在的表.其内容与真实的表相似,包含一系列带有名称的列和行数据.但是视图并不在数据库中以存储的数据的形式存在.行和列的数据来自定义视图时查询所引用的基本表,并 ...

  5. 快速入门Python中文件读写IO是如何来操作外部数据的?

    读写文件是最常见的IO操作.Python内置了读写文件的函数,用法和C是兼容的. 读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘, ...

  6. 转:zero length array问题

    单看这文章的标题,你可能会觉得好像没什么意思.你先别下这个结论,相信这篇文章会对你理解C语言有帮助.这篇文章产生的背景是在微博上,看到@Laruence同学出了一个关于C语言的题,微博链接.微博截图如 ...

  7. laravel command命令行

    生成类 为了创建一个新命令,你可以使用Artisan中的 command:make 命令生成一个骨架作为你的起点: 生成一个命令类 php artisan command:make FooComman ...

  8. go——基本构成要素

    Go的语言符号又称为词法元素,共包括5类内容: 标识符(identifier) 关键字(keyword) 字面量(literal) 分隔符(delimiter) 操作符(operator)它们可以组成 ...

  9. 解决hash冲突的办法

    1.开发定址法 2.再哈希法 3.链地址法 4.建立一个公共溢出区

  10. mybatis 复习笔记01

    本文内容转自传智播客笔记 1. 问题总结  1). 数据库连接,使用时就创建,不使用立即释放,对数据库进行频繁连接开启和关闭,造成数据库资源浪费,影响 数据库性能. 设想:使用数据库连接池管理数据库连 ...