摘要:本文主要向大家介绍了分布式锁学习之JAVA语言非常用技术ZooKeeper,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。
本文主要向大家介绍了分布式锁学习之JAVA语言非常用技术ZooKeeper,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。
由于在平时的工作中,线上服务器是分布式多台部署的,经常会面临解决分布式场景下数据一致性的问题,那么就要利用分布式锁来解决这些问题。以自己结合实际工作中的一些经验和网上看到的一些资料,做一个讲解和总结。之前我已经写了一篇关于分布式锁的文章:分布式锁1 Java常用技术方案。上一篇文章中主要写的是在日常项目中,较为常见的几种实现分布式锁的方法。通过这些方法,基本上可以解决我们日常工作中大部分场景下使用分布式锁的问题。
本篇文章主要是在上一篇文章的基础上,介绍一些虽然日常工作中不常用或者比较实现起来比较重,但是可以作为技术方案学习了解一下的分布式锁方案。希望这篇文章可以方便自己以后查阅,同时要是能帮助到他人那也是很好的。
正文:
第一步,使用zookeeper节点名称唯一性,用于分布式锁:
zookeeper抽象出来的节点结构是一个和文件系统类似的小型的树状的目录结构,同时zookeeper机制规定:同一个目录下只能有一个唯一的文件名。例如:我们在zookeeper的根目录下,由两个客户端同时创建一个名为/myDistributeLock,只有一个客户端可以成功。
上述方案和memcached的add()方法、redis的setnx()方法实现分布式锁有着相同的思路。这样的方案实现起来如果不考虑搭建和维护zookeeper集群的成本,由于正确性和可靠性是zookeeper机制自己保证的,实现还是比较简单的。
第二步,使用zookeeper临时顺序节点,用于分布式锁:
在讨论这套方案之前,我们有必要先“吹毛求疵”般的说明一下使用zookeeper节点名称唯一性来做分布式锁这个方案的缺点。比如,当许多线程在等待一个锁时,如果锁得到释放的时候,那么所有客户端都被唤醒,但是仅仅有一个客户端得到锁。在这个过程中,大量的线程根本没有获得锁的可能性,但是也会引起大量的上下文切换,这个系统开销也是不小的,对于这样的现象有一个专业名词,称之为“惊群效应”。
我们首先说明一下zookeeper的顺序节点、临时节点和watcher机制:
所谓顺序节点,假如我们在/myDisLocks/目录下创建3个节点,zookeeper集群会按照发起创建的顺序来创建节点,节点分别为/myDisLocks/0000000001、/myDisLocks/0000000002、/myDisLocks/0000000003。
所谓临时节点,临时节点由某个客户端创建,当客户端与zookeeper集群断开连接,则该节点自动被删除。
所谓对于watcher机制,大家可以参考Apache ZooKeeper Watcher机制源码解释。当然如果你之前不知道watcher机制是个什么东东,不建议你直接去看前边我提供的文章链接,这样你极有可能忘掉我们的讨论主线,即分布式锁的实现方案,而陷入到watcher机制的源码实现中。所以你也可以先看看下面的具体方案,猜测一下watcher是用来干嘛的,我这里先总结一句话做个引子: 所谓watcher机制,你可以简单一点儿理解成任何一个连接zookeeper的客户端可以通过watcher机制关注自己感兴趣的节点的增删改查,当这个节点发生增删改查的操作时,会“广播”自己的消息,所有对此感兴趣的节点可以在收到这些消息后,根据自己的业务需要执行后续的操作。
具体的使用步骤如下:
1. 每个业务线程调用create()方法创建名为“/myDisLocks/thread”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL,即节点类型为临时顺序节点。此时/myDisLocks节点下会出现诸如/myDisLocks/thread0000000001、/myDisLocks/thread0000000002、/myDisLocks/thread0000000003这样的子节点。
2. 每个业务线程调用getChildren(“myDisLocks”)方法来获取/myDisLocks这个节点下所有已经创建的子节点。
3. 每个业务线程获取到所有子节点的路径之后,如果发现自己在步骤1中创建的节点的尾缀编号是所有节点中序号最小的,那么就认为自己获得了锁。
4.如果在步骤3中发现自己并非是所有子节点中序号最小的,说明自己还没有获取到锁。使用watcher机制监视比自己创建节点的序列号小的节点(比自己创建的节点小的最大节点),进入等待。比如,如果当前业务线程创建的节点是/myDisLocks/thread0000000003,那么在没有获取到锁的情况下,他只需要监视/myDisLocks/thread0000000002的情况。只有当/myDisLocks/thread0000000002获取到锁并释放之后,当前业务线程才启动获取锁,这样可以避免一个业务线程释放锁之后,其他所有线程都去竞争锁,引起不必要的上下文切换,最终造成“惊群现象”。
5.释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可。
注意:这个方案实现的分布式锁还带着一点儿公平锁的味道!为什么呢?我们在利用每个节点的序号进行排队以此来避免进群现象时,实际上所有业务线程获得锁的顺序就是自己创建节点的顺序,也就是哪个业务线程先来,哪个就可以最快获得锁。
下面贴出我自己实现的上述方案的代码:
1. 代码中有两个Java类: MyDistributedLockByZK.java和LockWatcher.java。其中MyDistributedLockByZK.java中的main函数利用线程池启动5个线程,以此来模拟多个业务线程竞争锁的情况;而LockWatcher.java定义分布式锁和实现了watcher机制。
2. 同时,我使用的zookeeper集群是自己以前利用VMWare搭建的集群,所以zookeeper链接是192.168.224.170:2181,大家可以根据替换成自己的zookeeper链接即可。
1 public class MyDistributedLockByZK {
2 /** 线程池 **/
3 private static ExecutorService executorService = null;
4 private static final int THREAD_NUM = 5;
5 private static int threadNo = 0;
6 private static CountDownLatch threadCompleteLatch = new CountDownLatch(THREAD_NUM);
7
8 /** ZK的相关配置常量 **/
9 private static final String CONNECTION_STRING = "192.168.224.170:2181";
10 private static final int SESSION_TIMEOUT = 10000;
11 // 此变量在LockWatcher中也有一个同名的静态变量,正式使用的时候,提取到常量类中共同维护即可。
12 private static final String LOCK_ROOT_PATH = "/myDisLocks";
13
14 public static void main(String[] args) {
15 // 定义线程池
16 executorService = Executors.newFixedThreadPool(THREAD_NUM, new ThreadFactory() {
17 @Override
18 public Thread newThread(Runnable r) {
19 String name = String.format("第[%s]个测试线程", ++threadNo);
20 Thread ret = new Thread(Thread.currentThread().getThreadGroup(), r, name, 0);
21 ret.setDaemon(false);
22 return ret;
23 }
24 });
25
26 // 启动线程
27 if (executorService != null) {
28 startProcess();
29 }
30 }
31
32 /**
33 * @author zhangyi03
34 * @date 2017-5-23 下午5:57:27
35 * @description 模拟并发执行任务
36 */
37 public static void startProcess() {
38 Runnable disposeBusinessRunnable= new Thread(new Runnable() {
39 public void run() {
40 String threadName = Thread.currentThread().getName();
41
42 LockWatcher lock = new LockWatcher(threadCompleteLatch);
43 try {
44 /** 步骤1: 当前线程创建ZK连接 **/
45 lock.createConnection(CONNECTION_STRING, SESSION_TIMEOUT);
46
47 /** 步骤2: 创建锁的根节点 **/
48 // 注意,此处创建根节点的方式其实完全可以在初始化的时候由主线程单独进行根节点的创建,没有必要在业务线程中创建。
49 // 这里这样写只是一种思路而已,不必局限于此
50 synchronized (MyDistributedLockByZK.class){
51 lock.createPersistentPath(LOCK_ROOT_PATH, "该节点由" + threadName + "创建", true);
52 }
53
54 /** 步骤3: 开启锁竞争并执行任务 **/
55 lock.getLock();
56 } catch (Exception e) {
57 e.printStackTrace();
58 }
59 }
60 });
61
62 for (int i = 0; i < THREAD_NUM; i++) {
63 executorService.execute(disposeBusinessRunnable);
64 }
65 executorService.shutdown();
66
67 try {
68 threadCompleteLatch.await();
69 System.out.println("所有线程运行结束!");
70 } catch (InterruptedException e) {
71 e.printStackTrace();
72 }
73 }
74 }
本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注编程语言JAVA频道!
您输入的评论内容中包含违禁敏感词
我知道了
请输入正确的手机号码
请输入正确的验证码
您今天的短信下发次数太多了,明天再试试吧!
我们会在第一时间安排职业规划师联系您!
您也可以联系我们的职业规划师咨询:
版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
沪公网安备 31011502005948号