-
Notifications
You must be signed in to change notification settings - Fork 3.8k
2020 05 21 基于Zookeeper的分布式锁(干货)
为什么使用锁
锁的出现是为了解决资源争用问题,在单进程环境下的资源争夺可以使用 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)!!!
小傅哥(微信:fustack),公众号:bugstack虫洞栈
| bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!
🌏 知识星球:码农会锁
实战项目:「DDD+RPC分布式抽奖系统
」、专属小册、问题解答、简历指导、架构图稿、视频课程
🐲 头条
-
💥
🎁 Lottery 抽奖系统
- 基于领域驱动设计的四层架构的互联网分布式开发实践 -
小傅哥的《重学 Java 设计模式》
- 全书彩印、重绘类图、添加内容 -
⭐小傅哥的《Java 面经手册》
- 全书5章29节,417页11.5万字,完稿&发版 -
小傅哥的《手撸 Spring》
- 通过带着读者手写简化版 Spring 框架,了解 Spring 核心原理 -
🌈小傅哥的《SpringBoot 中间件设计和开发》
- 小册16个中间件开发30个代码库
⛳ 目录
💋 精选
🐾 友链
建立本开源项目的初衷是基于个人学习与工作中对 Java 相关技术栈的总结记录,在这里也希望能帮助一些在学习 Java 过程中遇到问题的小伙伴,如果您需要转载本仓库的一些文章到自己的博客,请按照以下格式注明出处,谢谢合作。
作者:小傅哥
链接:https://bugstack.cn
来源:bugstack虫洞栈
2021年10月24日,小傅哥
的文章全部开源到代码库 CodeGuide
中,与同好同行,一起进步,共同维护。
这里我提供 3 种方式:
-
提出
Issue
:在 Issue 中指出你觉得需要改进/完善的地方(能够独立解决的话,可以在提出 Issue 后再提交PR
)。 -
处理
Issue
: 帮忙处理一些待处理的Issue
。 -
提交
PR
: 对于错别字/笔误这类问题可以直接提交PR
,无需提交Issue
确认。
详细参考:CodeGuide 贡献指南 - 非常感谢你的支持,这里会留下你的足迹
- 加群交流 本群的宗旨是给大家提供一个良好的技术学习交流平台,所以杜绝一切广告!由于微信群人满 100 之后无法加入,请扫描下方二维码先添加作者 “小傅哥” 微信(fustack),备注:加群。
- 公众号(bugstack虫洞栈) - 沉淀、分享、成长,专注于原创专题案例,以最易学习编程的方式分享知识,让自己和他人都能有所收获。
感谢以下人员对本仓库做出的贡献或者对小傅哥的赞赏,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 Issue 或者 PR,请与我联系。