【中间件】Zookeeper从0到1实现一个分布式锁
分布式锁,在理论的业务应用场景中算是比拟罕用的了,而分布式锁的实现,常见的除了redis之外,就是zk的实现了,后面一篇博文介绍了zk的基本概念与应用姿态,那么如果让咱们来记住zk的个性来设计一个分布式锁,能够怎么做呢?
<!-- more -->
I. 方案设计
1. 创立节点形式实现
zk有四种节点,一个最容易想到的策略就是创立节点,谁创立胜利了,就示意谁持有了这个锁
这个思路与redis的setnx
有点类似,因为zk的节点创立,也只会有一个会话会创立胜利,其余的则会抛已存在的异样
借助长期节点,会话丢掉之后节点删除,这样能够防止持有锁的实例异样而没有被动开释导致所有实例都无奈持有锁的问题
如果采纳这种计划,如果我想实现阻塞获取锁的逻辑,那么其中一个计划就须要写一个while(true)来一直重试
while(true) { if (tryLock(xxx)) return true; else Thread.sleep(1000);}
另外一个策略则是借助事件监听,当节点存在时,注册一个节点删除的触发器,这样就不须要我本人重试判断了;充沛借助zk的个性来实现异步回调
public void lock() { if (tryLock(path, new Watcher() { @Override public void process(WatchedEvent event) { synchronized (path){ path.notify(); } } })) { return true; } synchronized (path) { path.wait(); }}
那么下面这个实现有什么问题呢?
每次节点的变更,那么所有的都会监听到变动,益处是非偏心锁的反对;毛病就是剩下这些唤醒的实例中也只会有一个抢占到锁,无意义的唤醒节约性能
2. 长期程序节点形式
接下来这种计划更加常见,早晨大部分的教程也是这种case,次要思路就是创立长期程序节点
只有序号最小的节点,才示意抢占锁胜利;如果不是最小的节点,那么就监听它后面一个节点的删除事件,后面节点删除了,一种可能是他放弃抢锁,一种是他开释本人持有的锁,不管哪种状况,对我而言,我都须要捞一下所有的节点,要么拿锁胜利;要么换一个前置节点
II.分布式锁实现
接下来咱们来一步步看下,基于长期程序节点,能够怎么实现分布式锁
对于zk,咱们仍然采纳apache的提供的包 zookeeper
来操作;后续提供Curator
的分布式锁实例
1. 依赖
外围依赖
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper --><dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.7.0</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions></dependency>
版本阐明:
- zk版本: 3.6.2
- SpringBoot: 2.2.1.RELEASE
2. 简略的分布式锁
第一步,都是实例创立
public class ZkLock implements Watcher { private ZooKeeper zooKeeper; // 创立一个长久的节点,作为分布式锁的根目录 private String root; public ZkLock(String root) throws IOException { try { this.root = root; zooKeeper = new ZooKeeper("127.0.0.1:2181", 500_000, this); Stat stat = zooKeeper.exists(root, false); if (stat == null) { // 不存在则创立 createNode(root, true); } } catch (Exception e) { e.printStackTrace(); } } // 简略的封装节点创立,这里只思考长久 + 长期程序 private String createNode(String path, boolean persistent) throws Exception { return zooKeeper.create(path, "0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, persistent ? CreateMode.PERSISTENT : CreateMode.EPHEMERAL_SEQUENTIAL); }}
在咱们的这个设计中,咱们须要持有以后节点和监听前一个节点的变更,所以咱们在ZkLock实例中,增加两个成员
/** * 以后节点 */private String current;/** * 前一个节点 */private String pre;
接下来就是尝试获取锁的逻辑
- current不存在,在示意没有创立过,就创立一个长期程序节点,并赋值current
- current存在,则示意之前曾经创立过了,目前处于期待锁开释过程
- 接下来依据以后节点程序是否最小,来表明是否持有锁胜利
- 当程序不是最小时,找后面那个节点,并赋值 pre;
- 监听pre的变动
/** * 尝试获取锁,创立程序长期节点,若数据最小,则示意抢占锁胜利;否则失败 * * @return */public boolean tryLock() { try { String path = root + "/"; if (current == null) { // 创立长期程序节点 current = createNode(path, false); } List<String> list = zooKeeper.getChildren(root, false); Collections.sort(list); if (current.equalsIgnoreCase(path + list.get(0))) { // 获取锁胜利 return true; } else { // 获取锁失败,找到前一个节点 int index = Collections.binarySearch(list, current.substring(path.length())); // 查问以后节点后面的那个 pre = path + list.get(index - 1); } } catch (Exception e) { e.printStackTrace(); } return false;}
请留神下面的实现,这里并没有去监听前一个节点的变更,在设计tryLock
,因为是立马返回胜利or失败,所以应用这个接口的,不须要注册监听
咱们的监听逻辑,放在 lock()
同步阻塞外面
- 尝试抢占锁,胜利则间接返回
- 拿锁失败,则监听前一个节点的删除事件
public boolean lock() { if (tryLock()) { return true; } try { // 监听前一个节点的删除事件 Stat state = zooKeeper.exists(pre, true); if (state != null) { synchronized (pre) { // 阻塞期待后面的节点开释 pre.wait(); // 这里不间接返回true,因为后面的一个节点删除,可能并不是因为它持有锁并开释锁,如果是因为这个会话中断导致长期节点删除,这个时候须要做的是换一下监听的 preNode return lock(); } } else { // 不存在,则再次尝试拿锁 return lock(); } } catch (Exception e) { e.printStackTrace(); } return false;}
留神:
- 当节点不存在时,或者事件触发回调之后,从新调用
lock()
,表明我胡汉三又来竞争锁了?
为啥不是间接返回 true? 而是须要从新竞争呢?
- 因为后面节点的删除,有可能是因为后面节点的会话中断导致的;然而锁还在另外的实例手中,这个时候我应该做的是从新排队
最初别忘了开释锁
public void unlock() { try { zooKeeper.delete(current, -1); current = null; zooKeeper.close(); } catch (Exception e) { e.printStackTrace(); }}
到此,咱们的分布式锁就实现了,接下来咱们复盘下实现过程
- 所有知识点来自前一篇的zk根底应用(创立节点,删除节点,获取所有本人点,监听事件)
- 抢锁过程 =》 创立序号最小的节点
- 若节点不是最小的,那么就监听后面的节点删除事件
这个实现,反对了锁的重入(why? 因为锁未开释时,咱们保留了current,以后节点存在时则直接判断是不是最小的;而不是从新创立)
3. 测试
最初写一个测试case,来看下
@SpringBootApplicationpublic class Application { private void tryLock(long time) { ZkLock zkLock = null; try { zkLock = new ZkLock("/lock"); System.out.println("尝试获取锁: " + Thread.currentThread() + " at: " + LocalDateTime.now()); boolean ans = zkLock.lock(); System.out.println("执行业务逻辑:" + Thread.currentThread() + " at:" + LocalDateTime.now()); Thread.sleep(time); } catch (Exception e) { e.printStackTrace(); } finally { if (zkLock != null) { zkLock.unlock(); } } } public Application() throws IOException, InterruptedException { new Thread(() -> tryLock(10_000)).start(); Thread.sleep(1000); // 获取锁到执行锁会有10s的距离,因为下面的线程抢占到锁,并持有了10s new Thread(() -> tryLock(1_000)).start(); System.out.println("---------over------------"); Scanner scanner = new Scanner(System.in); String ans = scanner.next(); System.out.println("---> over --->" + ans); } public static void main(String[] args) { SpringApplication.run(Application.class); }}
输入后果如下
II. 其余
0. 我的项目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 我的项目源码:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/411-zookeeper-distributelock
1. 一灰灰Blog
尽信书则不如,以上内容,纯属一家之言,因集体能力无限,不免有疏漏和谬误之处,如发现bug或者有更好的倡议,欢送批评指正,不吝感谢
上面一灰灰的集体博客,记录所有学习和工作中的博文,欢送大家前去逛逛
- 一灰灰Blog集体博客 https://blog.hhui.top
- 一灰灰Blog-Spring专题博客 http://spring.hhui.top