Skip to content

2020 05 21 基于Zookeeper的分布式锁(干货)

fuzhengwei edited this page May 21, 2020 · 2 revisions

基于Zookeeper的分布式锁(干干干货)

原文地址: https://juejin.im/post/5df883d96fb9a0163514d97f

介绍

为什么使用锁

锁的出现是为了解决资源争用问题,在单进程环境下的资源争夺可以使用 JDK里的锁实现.

为什么使用分布式锁?

顾名思义,分布式锁是为了分布式环境下的资源争用问题.

Zookeeper是如何实现分布式锁的?

基于Zookeeper的分布式锁都是依赖于zk节点路径唯一的机制来实现的.

什么意思呢?

就是在zk中,在分布式锁的场景下 对于同一个路径,只能有一个客户端能创建成功,其它的都创建失败.(这个不难理解,在平时系统中也没见过有哪2个文件地址完全相同)

下面就说一下zk分布式锁2种实现,没错 本篇就是干的不能再干的干货!!!

第一种分布式锁

具体流程

第一种实现是利用的zk的临时节点, 在争抢锁的时候,所有的客户端都尝试创建一个临时节点(代表锁住的资源),只有一个客户端会创建成功,创建成功的客户端得到锁,其它的客户端则监听(利用zk的watch)该节点的状态改变并且进入阻塞,节点改变后 zk server 会通知剩下的客户端,剩下的客户端停止阻塞并且重新争抢锁.

zk中有持久节点和临时节点,为什么使用临时节点呢?

如果使用的是持久节点,则这个节点在客户端下线后,依旧会一直存在,不会自动删除,导致 其它客户端一直无法争抢到锁 .如果使用的是临时节点的话, 在客户端下线后zk会删除与其相关的临时节点,这样其它客户端就能重新争抢锁 .

代码实现

	@Override
	public void lock() {
		// 如果获取不到锁,阻塞等待
		if (!tryLock()) {
			// 没获得锁,阻塞自己
			waitForLock();
			// 再次尝试
			lock();
		}
	}

	@Override
	public boolean tryLock() { // 不会阻塞
		// 创建节点
		try {
		  // 创建临时节点,zk中的节点(路径)唯一,只有一个会创建成功
      // 为什么使用临时节点: 客户端掉线后会自动删除节点(释放锁)
			client.createEphemeral(lockPath);
		} catch (ZkNodeExistsException e) {
			return false;
		}
		return true;
	}

	/**
   * 争抢不到锁的话,等待锁的释放
	 */
	private void waitForLock() {
		CountDownLatch cdl = new CountDownLatch(1);
		IZkDataListener listener = new IZkDataListener() {
			@Override
			public void handleDataDeleted(String dataPath) throws Exception {
				System.out.println("收到节点被删除的消息,停止等待,重新争夺锁");
				cdl.countDown();
			}
			@Override
			public void handleDataChange(String dataPath, Object data)
					throws Exception {
			}
		};

		// 监听
		client.subscribeDataChanges(lockPath, listener);
		// 判断锁节点是否存在,存在的话表明有别人
		if (this.client.exists(lockPath)) {
			try {
			    // 等待接收到消息后,继续往下执行
				cdl.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// 取消监听消息
		client.unsubscribeDataChanges(lockPath, listener);
	}

总结一下

实现简单,但是会有 羊群效应 ,节点的删除都会通知所有的客户端,并且所有的客户端会 取消监听 + 重新一起争夺锁 + 争夺失败 + 再次开启监听 ,如此循环,资源耗费多,并且这种耗费是可以避免的,那么如何避免呢?就是下面第二种的 改进版分布式锁 .

第二种分布式锁

这一种分布式锁的实现是利用zk的临时顺序节点,每一个客户端在争夺锁的时候都由zk分配一个顺序号(sequence),客户端则按照这个顺序去获取锁.

具体流程

lock跟前面的一样,不过lockPath(锁住的资源)是一个持久节点,客户端在该持久节点下面创建临时顺序节点,获取到顺序号后,根据自己是否是最小的顺序号来获取锁,顺序号最小则获取锁,序号不为最小则监听(watch)前一个顺序号,当前一个顺序号被删除的时候表明锁被释放了,则会通知下一个客户端.

代码实现

下面贴出跟第一种实现不同的代码

	/**
	 * 尝试加锁
	 *
	 * @return
	 */
	@Override
	public boolean tryLock() {
		// 创建临时顺序节点
		if (this.currentPath == null) {
			// 在lockPath节点下面创建临时顺序节点
			currentPath = this.client.createEphemeralSequential(LockPath + "/", "aaa");
		}
		// 获得所有的子节点
		List<String> children = this.client.getChildren(LockPath);

		// 排序list
		Collections.sort(children);

		// 判断当前节点是否是最小的,如果是最小的节点,则表明此这个client可以获取锁
		if (currentPath.equals(LockPath + "/" + children.get(0))) {
			return true;	
		} else {
			// 如果不是当前最小的sequence,取到前一个临时节点
			// 1.单独获取临时节点的顺序号
			// 2.查找这个顺序号在children中的下标
			// 3.存储前一个节点的完整路径
			int curIndex = children.indexOf(currentPath.substring(LockPath.length() + 1));
			beforePath = LockPath + "/" + children.get(curIndex - 1);
		}
		return false;
	}

	private void waitForLock() {
		CountDownLatch cdl = new CountDownLatch(1);
		// 注册watcher
		IZkDataListener listener = new IZkDataListener() {
			@Override
			public void handleDataDeleted(String dataPath) throws Exception {
				System.out.println("监听到前一个节点被删除了");
				cdl.countDown();
			}
			@Override
			public void handleDataChange(String dataPath, Object data) throws Exception {
			}
		};

		// 监听前一个临时节点
		client.subscribeDataChanges(this.beforePath, listener);

		// 前一个节点还存在,则阻塞自己
		if (this.client.exists(this.beforePath)) {
			try {
				// 直至前一个节点释放锁,才会继续往下执行
				cdl.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		// 醒来后,表明前一个临时节点已经被删除,此时客户端可以获取锁 && 取消watcher监听
		client.unsubscribeDataChanges(this.beforePath, listener);
	}

总结一下

实现比第一种复杂一点,但是更加的合理,少做了很多不必要的操作,只唤醒了后面一个客户端.

总结

由于zk自身的设计,zk不适合高并发写,需要在使用zk分布式锁前先做一定过滤操作,先过滤掉部分请求,再进行锁争夺.

分布式锁当然不止zk的实现,各个实现都有其适用的场景,在分布式系统中,没有最完美的方案,只有最合适的方案,往往都是取舍问题.

最后

最后的最后,非常感谢你们能看到这里!!你们的阅读都是对作者的一次肯定!!!
觉得文章有帮助的看官顺手点个赞再走呗(终于暴露了我就是来骗赞的(◒。◒)),你们的每个赞对作者来说都非常重要(异常真实),都是对作者写作的一次肯定(double)!!!

📝 首页

🌏 知识星球码农会锁

实战项目:「DDD+RPC分布式抽奖系统」、专属小册、问题解答、简历指导、架构图稿、视频课程

🐲 头条

⛳ 目录

  1. 源码 - :octocat: 公众号:bugstack虫洞栈 文章所涉及到的全部开源代码
  2. Java
  3. Spring
  4. 面向对象
  5. 中间件
  6. Netty 4.x
  7. 字节码编程
  8. 💯实战项目
  9. 部署 Dev-Ops
  10. 📚PDF 下载
  11. 关于

💋 精选

🐾 友链

建立本开源项目的初衷是基于个人学习与工作中对 Java 相关技术栈的总结记录,在这里也希望能帮助一些在学习 Java 过程中遇到问题的小伙伴,如果您需要转载本仓库的一些文章到自己的博客,请按照以下格式注明出处,谢谢合作。

作者:小傅哥
链接:https://bugstack.cn
来源:bugstack虫洞栈

2021年10月24日,小傅哥 的文章全部开源到代码库 CodeGuide 中,与同好同行,一起进步,共同维护。

这里我提供 3 种方式:

  1. 提出 Issue :在 Issue 中指出你觉得需要改进/完善的地方(能够独立解决的话,可以在提出 Issue 后再提交 PR )。
  2. 处理 Issue : 帮忙处理一些待处理的 Issue
  3. 提交 PR: 对于错别字/笔误这类问题可以直接提交PR,无需提交Issue 确认。

详细参考:CodeGuide 贡献指南 - 非常感谢你的支持,这里会留下你的足迹

  • 加群交流 本群的宗旨是给大家提供一个良好的技术学习交流平台,所以杜绝一切广告!由于微信群人满 100 之后无法加入,请扫描下方二维码先添加作者 “小傅哥” 微信(fustack),备注:加群。
微信:fustack

  • 公众号(bugstack虫洞栈) - 沉淀、分享、成长,专注于原创专题案例,以最易学习编程的方式分享知识,让自己和他人都能有所收获。
公众号:bugstack虫洞栈

感谢以下人员对本仓库做出的贡献或者对小傅哥的赞赏,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 Issue 或者 PR,请与我联系。

Clone this wiki locally