原理
基本方案是基于ZooKeeper的临时节点与和watch机制。当要获取锁时在某个目录下创建一个临时节点,创建成功则表示获取锁成功,创建失败则表示获取锁失败,此时watch该临时节点,当该临时节点被删除后再去尝试获取锁。临时节点好处在于,当客户端崩溃后自动删除临时节点的同时锁也被释放了。该方案有个缺点缺点,就是大量客户端监听同一个节点,当锁释放后所有等待的客户端同时尝试获取锁,并发量很大。因此有了以下优化的方案。
优化方案基于ZooKeeper的临时有序节点和watch机制。当要获取锁时在某个目录下创建一个临时有序节点,每次创建均能成功,只是返回的节点序号不同。只有返回的节点序号是该目录下最小的才表示获取锁成功,否则表示获取锁失败,此时watch节点序号比本身小的前一个节点,当watch的节点被删除后再去尝试获取锁。
示例
1.使用Maven引入ZooKeeper客户端:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.6</version>
</dependency>
2.工具类
package com.example.demo;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ZkLockUtils implements Watcher {
private ZooKeeper zk;
/**
* 锁根节点
*/
private String lockRoot;
/**
* 锁子节点前缀
*/
private String lock;
/**
* 锁子节点分隔字符串
* 用于筛选锁根节点下和锁相关的子节点
*/
private String splitStr;
/**
* 要监听的锁子节点
*/
private String watchNode;
/**
* 当前创建的锁子节点
*/
private String currentNode;
/**
* 等待计数器
*/
private CountDownLatch latch;
/**
* 初始化
*
* @param connectString 连接字符串,如果多个则使用逗号分隔,例如127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002
* @param lockRoot 锁根节点,“/”开头
* @param lock 锁子节点前缀,不带“/”
* @param splitStr 锁子节点分隔字符串
* @throws Exception Exception
*/
public ZkLockUtils(String connectString, String lockRoot, String lock, String splitStr) throws Exception {
this.lockRoot = lockRoot;
this.lock = lock;
if (lock.contains(splitStr)) {
throw new RuntimeException("lock不能包含splitStr");
}
this.splitStr = splitStr;
// 会话超时时间30秒
int sessionTimeout = 30000;
// 初始化zk客户端,监视器设置为当前类
zk = new ZooKeeper(connectString, sessionTimeout, this);
// 如果锁根节点不存在,则创建
Stat stat = zk.exists(lockRoot, false);
if (stat == null) {
zk.create(lockRoot, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
/**
* 收到通知时执行
*/
public void process(WatchedEvent event) {
if (latch != null) {
latch.countDown();
}
}
/**
* 获取锁,如果超过等待时间,则获取锁失败
*
* @param waitTime 等待时间
* @param timeUnit 时间单位
* @return true(成功) false(失败)
* @throws Exception Exception
*/
public boolean lock(long waitTime, TimeUnit timeUnit) throws Exception {
// 创建临时有序锁子节点
currentNode = zk.create(lockRoot + "/" + lock + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 取出所有锁根节点的子节点
List<String> childNodes = zk.getChildren(lockRoot, false);
// 筛选锁根节点下和锁相关的子节点
List<String> lockChildNodes = new ArrayList<>();
for (String childNode : childNodes) {
String str = childNode.split(splitStr)[0];
if (str.equals(lock)) {
lockChildNodes.add(childNode);
}
}
// 升序排序
Collections.sort(lockChildNodes);
// 如果是最小的锁子节点,则获得锁
if (currentNode.equals(lockRoot + "/" + lockChildNodes.get(0))) {
System.out.println("当前是最小的锁子节点,获取锁成功");
return true;
}
// 如果不是最小的锁子节点,找到索引比自己小1的锁子节点
String previousNode = currentNode.substring(currentNode.lastIndexOf("/") + 1);
watchNode = lockChildNodes.get(Collections.binarySearch(lockChildNodes, previousNode) - 1);
// 判断要监听的锁子节点是否存在并监听该节点,如果不存在则该节点已被删除,直接获得锁
Stat stat = zk.exists(lockRoot + "/" + watchNode, true);
if (stat != null) {
latch = new CountDownLatch(1);
boolean b = latch.await(waitTime, timeUnit);
latch = null;
if (b) {
System.out.println("监听到锁子节点被删除,获取锁成功");
return true;
} else {
System.out.println("等待超时,获取锁失败");
return false;
}
} else {
System.out.println("要监听的锁子节点不存在,获取锁成功");
return true;
}
}
/**
* 释放锁
*/
public void unlock() throws Exception {
// version为-1表示匹配任何版本
zk.delete(currentNode, -1);
watchNode = null;
currentNode = null;
zk.close();
}
}
3.测试
package com.example.demo;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Demo {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread thread1 = new Thread(() -> {
ZkLockUtils zkLockUtils = null;
try {
zkLockUtils = new ZkLockUtils("127.0.0.1:3181,127.0.0.1:3182,127.0.0.1:3183", "/locks", "lock", "_");
zkLockUtils.lock(5, TimeUnit.SECONDS);
Thread.sleep(10000);
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (zkLockUtils != null) {
try {
zkLockUtils.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
ZkLockUtils zkLockUtils = null;
try {
zkLockUtils = new ZkLockUtils("127.0.0.1:3181,127.0.0.1:3182,127.0.0.1:3183", "/locks", "lock", "_");
zkLockUtils.lock(5, TimeUnit.SECONDS);
Thread.sleep(10000);
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (zkLockUtils != null) {
try {
zkLockUtils.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
thread2.start();
countDownLatch.await();
System.out.println("所有子线程都执行完毕");
}
}
优缺点
1.Redis分布式锁需要轮询获取锁,性能开销较大。ZooKeeper分布式锁基于watch机制监听节点,不需要轮询获取锁,性能开销较小。
2.如果Redis获取锁的客户端非正常退出,那么只能等待超时时间之后才能释放锁。Redis因为创建的是临时节点,只要客户端崩溃或者连接断开,临时节点就会被删除,锁也就被立即释放了。
- Zookeeper是如何实现分布式锁的
[toc] Zookeeper是如何实现分布式锁的 标签 : Zookeeper 分布式 实现分布式锁要考虑的重要问题 1. 三个核心要素 加锁, 解锁, 锁超时 2. 三个问题 要保证原子性操作, ...
- zookeeper适用场景:分布式锁实现
问题导读:1.zookeeper如何实现分布式锁?2.什么是羊群效应?3.zookeeper如何释放锁? 在zookeeper应用场景有关于分布式集群配置文件同步问题的描述,设想一下如果有100台机器 ...
- 服务注册中心之ZooKeeper系列(三) 实现分布式锁
通过ZooKeeper的有序节点.节点路径不回重复.还有节点删除会触发Wathcer事件的这些特性,我们可以实现分布式锁. 一.思路 zookeeper中创建一个根节点Locks,用于后续各个客户端的 ...
- zookeeper【5】分布式锁
我们常说的锁是单进程多线程锁,在多线程并发编程中,用于线程之间的数据同步,保护共享资源的访问.而分布式锁,指在分布式环境下,保护跨进程.跨主机.跨网络的共享资源,实现互斥访问,保证一致性. 架构图: ...
- 基于zookeeper或redis实现分布式锁
前言 在分布式系统中,分布式锁是为了解决多实例之间的同步问题.例如master选举,能够获取分布式锁的就是master,获取失败的就是slave.又或者能够获取锁的实例能够完成特定的操作. 目前比较常 ...
- 基于ZooKeeper的三种分布式锁实现
[欢迎关注公众号:程序猿讲故事 (codestory),及时接收最新文章] 今天介绍基于ZooKeeper的分布式锁的简单实现,包括阻塞锁和非阻塞锁.同时增加了网上很少介绍的基于节点的非阻塞锁实现,主 ...
- Zookeeper绍二(分布式锁介)
一.为什么会有分布式锁? 在多线程环境下,由于上下文的切换,数据可能出现不一致的情况或者数据被污染,我们需要保证数据安全,所以想到了加锁. 所谓的加锁机制呢,就是当一个线程访问该类的某个数据时,进行保 ...
- 跟着实例学习ZooKeeper的用法: 分布式锁
锁 分布式的锁全局同步, 这意味着任何一个时间点不会有两个客户端都拥有相同的锁. 可重入锁Shared Reentrant Lock 首先我们先看一个全局可重入的锁. Shared意味着锁是全局可见的 ...
- Zookeeper系列3 实现分布式锁
基本思路 1 client调用create()方法创建“/locks/_lock_”临时顺序节点,注意节点类型是EPHEMERAL_SEQUENTIAL 2 client调用getChildren(& ...
- ZooKeeper 分布式锁实现
1 场景描述 在分布式应用, 往往存在多个进程提供同一服务. 这些进程有可能在相同的机器上, 也有可能分布在不同的机器上. 如果这些进程共享了一些资源, 可能就需要分布式锁来锁定对这些资源的访问. 2 ...
随机推荐
- Redis秒杀系统架构设计-微信抢红包
导读 前二天我写了一篇,Redis高级项目实战(点我直达),SpringBoot整合Redis附源码(点我直达),今天我们来做一下Redis秒杀系统的设计.当然啦,Redis基础知识还不过关的,先去加 ...
- 消息型中间件之RabbitMQ集群
在上一篇博客中我们简单的介绍了下rabbitmq简介,安装配置相关指令的说明以及rabbitmqctl的相关子命令的说明:回顾请参考https://www.cnblogs.com/qiuhom-187 ...
- 欢迎来到 C# 9.0(Welcome to C# 9.0)【纯手工翻译】
翻译自 Mads Torgersen 2020年5月20日的博文<Welcome to C# 9.0>,Mads Torgersen 是微软 C# 语言的首席设计师,也是微软 .NET 团 ...
- js判断传递参数的的类型,并返回
function type(target) { var templet ={ "[object Array]" : "arry"; "[obj ...
- 2020.08.23 瞎扯周记之论短暂假期(QAQ)内要不要睡午觉
蒟蒻的假期都是很短暂的嘛 作为一只合格的蒟蒻 假期自然是很短暂的QAQ 只有短短的26h93360s(手动微笑) 总体来讲 假期只有两件事要干: 1.满足人体自身需求 2.满足作业需求 2.5.摸鱼 ...
- 【python】我OUT了,原来函数中的冒号和箭头是这么回事
翻了翻httprunner的源代码,越看越不对劲,感觉有点看不懂语法了. 这都什么鬼?感觉心好慌,顿时感到惭愧万分,还好意思说自己了解Python呢. 赶紧了解一下,原来这叫 type hints,是 ...
- FZU - 2038 -E - Another Postman Problem (思维+递归+回溯)
Chinese Postman Problem is a very famous hard problem in graph theory. The problem is to find a shor ...
- Inscribed Figures(思维)
The math faculty of Berland State University has suffered the sudden drop in the math skills of enro ...
- 1090 Highest Price in Supply Chain (25 分)(模拟建树,找树的深度)牛客网过,pat没过
A supply chain is a network of retailers(零售商), distributors(经销商), and suppliers(供应商)-- everyone invo ...
- 深入了解Redis【二】对象及数据结构综述
引言 Redis中每个键值对都是由对象组成: 键总是一个字符串对象(string) 值可以是字符串对象(string).列表对象(list).哈希对象(hash).集合对象(set).有序集合对象(z ...