zookeeper实现分布式锁
文章目录
一:锁的特点与原生zookeeper
1:普通锁有什么特点
2:为什么zookeeper可以用来实现锁
- 同一个父目录下面不能有相同的子节点,这就是zookeeper的排他性
- 通过JDK的栅栏来实现阻塞性
- 可重入性我们可以通过计数器来实现【count += 1】
3:创建客户端的核心类:Zookeeper
org.apache.zookeeper
org.apache.zookeeper.data
下面是核心方法:
方法 | 作用 |
---|---|
connect | 连接到zk集合 |
create | 创建znode |
exist | 检查znode是否存在及其信息 |
getData | 从特定的znode获取数据 |
setData | 从特定的znode设置数据 |
getChildren | 获取特定znode中的所有子节点 |
delete | 删除特定znode及其所有子项 |
close | 关闭连接 |
二:使用第三方客户端zkClient来简化操作
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
1:实现序列化接口
package my_zk;
import org.I0Itec.zkclient.exception.ZkMarshallingError;
import org.I0Itec.zkclient.serialize.ZkSerializer;
import java.nio.charset.StandardCharsets;
/**
* <p>
* 功能描述:实现ZK序列化接口
* </p>
*
* @author cui haida
* @date 2024/03/04/11:19
*/
public class MyZkSerializer implements ZkSerializer {
/**
* 正常来说我们还需要进行一个非空判断,这里为了省事没做,不过严格来说是需要做的
* 就是简单的转换
*/
@Override
public byte[] serialize(Object data) throws ZkMarshallingError {
String d = (String) data;
return d.getBytes(StandardCharsets.UTF_8);
}
@Override
public Object deserialize(byte[] bytes) throws ZkMarshallingError {
return new String(bytes, StandardCharsets.UTF_8);
}
}
2:zkclient的简单使用
package my_zk;
import lombok.extern.slf4j.Slf4j;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.zookeeper.CreateMode;
import java.util.List;
/**
* <p>
* 功能描述:Zk客户端实现
* </p>
*
* @author cui haida
* @date 2024/03/04/11:23
*/
@Slf4j
public class ZkClientDemo {
public static void main(String[] args) {
// 创建一个zk客户端
ZkClient client = new ZkClient("localhost:2181");
// 实现序列化接口
client.setZkSerializer(new MyZkSerializer());
// 创建一个节点zk,在zk节点下再创建一个子节点app6,赋值123
// 在之前也已经提到了,zookeeper中的节点既是文件夹也是文件
// 源码中CreateMode是一个枚举,PERSISTENT---当客户端断开连接时,znode不会自动删除,也就是声明这个节点是一个持久节点
client.create("/zk/app6", "123", CreateMode.PERSISTENT);
client.subscribeChildChanges("/zk/app6", new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List<String> currentChildren) throws Exception {
System.out.println(parentPath + "子节点发生变化:" + currentChildren);
}
});
// 这里开始创建一个watch,但是为什么这个方法会命名为subscribeDataChanges,有如下的原因:
// 1:原本watch的设置然后获取是仅一次性的,现在我们使用subscribe这个英文,代表订阅,代表这个watch一直存在
// 2:使用这个方法我们可以轻易实现持续监听的效果,比原生zookeeper方便
client.subscribeDataChanges("zk/app6", new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) {
System.out.println(dataPath + "节点的值发生变化:" + data);
}
@Override
public void handleDataDeleted(String dataPath) {
System.out.println(dataPath + "节点被删除");
}
});
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
log.error("线程被异常中断");
e.printStackTrace();
}
}
}
测试监听事件
create /zk/app6/tellYourDream
时—控制台打印/zk/app6子节点发生变化:[tellYourDream]delete /zk/app6/tellYourDream
—控制台打印/zk/app6子节点发生变化:[],此时已经不存在任何节点,所以为空set /zk/app6 123456
—/zk/app6发生变化:123456delete /zk/app6
—同时触发了两个监听事件,/zk/app6子节点发生变化:null 和 /zk/app6节点被删除
3:CreateMode的补充
- 持久化节点:不删除节点永远存在。且可以创建子节点
- 非持久节点,换言之就是临时节点,临时节点就是客户端连接的时候创建,客户端挂起的时候,临时节点自动删除。不能创建子节点
三:Zookeeper实现分布式锁
1:节点不可重名+watch机制
之前有提到,zookeeper中同一个子节点下面的节点名称是不能相同的,我们可以利用这个互斥性,就可以实现分布式锁的工具
临时节点就是创建的时候存在,消失的时候,节点自动删除,当客户端失联,网络不稳定或者崩溃的时候,这个通过临时节点所创建的锁就会自行消除。这样就可以完美避免死锁的问题。所以我们利用这个特性,实现我们的需求。
原理其实就是节点不可重名+watch机制。
比如说我们的程序有多个服务实例,哪个服务实例都去创建一个lock节点,谁创建了,谁就获得了锁,剩下我们没有创建的应用,就去监听这个lock节点,如果这个lock节点被删除掉,这时可能出现两种情况,一就是客户端连不上了,另一种就是客户端释放锁,将lock节点给删除掉了。
package my_zk;
import cn.hutool.core.util.StrUtil;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* <p>
* 功能描述:
* </p>
*
* @author cui haida
* @date 2024/03/04/14:04
*/
public class ZkDistributeLock implements Lock {
// 我们需要一个锁的目录
private String lockPath;
// 我们需要一个客户端
private ZkClient client;
// 构造函数传值
public ZkDistributeLock(String lockPath) {
if (StrUtil.isBlank(lockPath)) {
throw new IllegalStateException("path不能为空白字符串");
}
this.lockPath = lockPath;
client = new ZkClient("localhost:2181");
client.setZkSerializer(new MyZkSerializer());
}
@Override
public void lock() {
if (!tryLock()) {
waitForLock(); // 如果没有获得锁,阻塞自己
lock(); // 从阻塞中唤醒,再次尝试获得锁
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
client.createEphemeral(lockPath); // 创建临时节点
} catch (ZkNodeExistsException e) {
return false;
}
return true;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
client.delete(lockPath);
}
@Override
public Condition newCondition() {
return null;
}
private void waitForLock() {
final 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 {
}
};
// 订阅数据变化的监听,监听的父节点路径是lockPath
client.subscribeDataChanges(lockPath, listener);
// 阻塞自己
if (this.client.exists(lockPath)) {
try {
cdl.await(); // 直到监听的前一个节点被唤醒,调用了countDown()才会跳出阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 取消注册
client.unsubscribeDataChanges(lockPath, listener);
}
}
获取锁,创建节点后
1.成功获取到的---执行业务---然后释放锁
|
|
|
2.获取失败,注册节点的watch---阻塞等待---取消watch---再回到获取锁,创建节点的判断
惊群效应
比如我的实例现在有无数个,此时我们的lock每次被创建,有人获取了锁之后,其他的人都要被通知阻塞,此时我们就浪费了很多的网络资源,也就是惊群效应。
2:取号 + 最小号取lock + watch
package com.atguigu.case2;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* 分布式锁
* @author cuihaida
*/
public class DistributedLock {
private ZooKeeper zooKeeper;
private CountDownLatch countDownLatch = new CountDownLatch(1);
private CountDownLatch waitLatch = new CountDownLatch(1);
private String waitPath;
private String curNode;
public DistributedLock() throws IOException, InterruptedException, KeeperException {
// 获取连接
// 注意:逗号的左右两端不能又空格
String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
int sessionTimeout = 2000;
// 连接
zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 如果连接上zk了,可以释放
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
countDownLatch.countDown();
}
// 存在节点删除,并且删除的是当前节点监听的前一个节点时候,countDown
if (watchedEvent.getType() == Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)) {
waitLatch.countDown();
}
}
});
// 成功连接后,才往下走
countDownLatch.await();
// 判断根节点的/locks是不是存在
Stat exists = zooKeeper.exists("/locks", false);
if (exists == null) {
// 创建根节点
String locks = "locks";
zooKeeper.create("/locks", locks.getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT
);
}
}
/**
* 核心逻辑,zk分布式锁
* 1:创建临时的,带有序号的节点
* 2:判断当前创建的节点是不是最小的序号节点
* 如果是,就获取到锁
* 如果不是,就监听他序号的前一个
*/
public void zkLock() throws KeeperException, InterruptedException {
// 创建临时带序号的节点
curNode = zooKeeper.create("/locks/seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 判断当前创建的节点是不是最小的序号节点,如果是就获取到锁,如果不是,就监听他序号的前一个
List<String> children = zooKeeper.getChildren("/locks", false);
if (children.size() == 1) {
return ;
} else {
Collections.sort(children); // 通过需要排序
String thisNode = curNode.substring("/locks".length());
int index = children.indexOf(thisNode);
if (index == -1) {
System.out.println("数据异常");
} else if (index == 0) {
return ;
} else {
// 监听他的前一个节点
waitPath = "/locks/" + children.get(index - 1);
zooKeeper.getData(waitPath, true, null);
}
}
// 只有前一个节点删除的时候往下走
waitLatch.await();
}
/**
* 解锁就是从删除当前加锁
*/
public void zkUnlock() throws KeeperException, InterruptedException {
// 删除正在加锁的节点节点
zooKeeper.delete(curNode, -1);
}
}
3:更为简单的第三方客户端—Curator
<!-- curator -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
<version>5.3.0</version>
</dependency>
package com.atguigu.case3;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
/**
* curator框架测试
* @author cuihaida
*/
public class CuratorLockTest {
public static void main(String[] args) {
// 创建分布式锁1
// 第一个参数是客户端,第二个参数是路径
InterProcessMutex interProcessMutex01 = new InterProcessMutex(getCuratorFramework(), "/locks");
// 创建分布式锁2
InterProcessMutex interProcessMutex02 = new InterProcessMutex(getCuratorFramework(), "/locks");
// -------------- 下面是测试 -----------------
new Thread(() -> {
try {
interProcessMutex01.acquire();
System.out.println("线程1启动, lock1获取到锁");
interProcessMutex01.acquire();
System.out.println("线程1启动, lock1再次获取到锁");
Thread.sleep(5000);
interProcessMutex01.release();
System.out.println("lock1释放锁");
interProcessMutex01.release();
System.out.println("lock1再次释放锁");
} catch (Exception e) {
System.out.println("error");
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
interProcessMutex02.acquire();
System.out.println("线程2启动, lock2获取到锁");
interProcessMutex02.acquire();
System.out.println("线程2启动, lock2再次获取到锁");
Thread.sleep(5000);
interProcessMutex02.release();
System.out.println("lock2释放锁");
interProcessMutex02.release();
System.out.println("lock2再次释放锁");
} catch (Exception e) {
System.out.println("error");
e.printStackTrace();
}
}).start();
}
/**
* 客户端连接
* @return 客户端
*/
private static CuratorFramework getCuratorFramework() {
String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
int sessionTimeout = 2000;
ExponentialBackoffRetry exponentialBackoffRetry = new ExponentialBackoffRetry(3000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
// 连接地址
.connectString(connectString)
// 超时时间
.sessionTimeoutMs(sessionTimeout)
// 重试设置
.retryPolicy(exponentialBackoffRetry)
.build();
client.start();
System.out.println("zookeeper启动成功");
return client;
}
}
四:zk分布式锁和redis分布式锁的对比
1:Redis 实现的分布式锁
Redis 实现的分布式锁的话,不能够 100% 保证可用性 ,因为在真实环境中使用分布式锁,一般都会集群部署 Redis ,来避免单点问题
那么 Redisson 去 Redis 集群上锁的话,先将锁信息写入到主节点中,如果锁信息还没来得及同步到从节点中,主节点就宕机了,就会导致这个锁信息丢失
并且在分布式环境下可能各个机器的时间不同步,都会导致加锁时出现一系列无法预知的问题
因此 RedLock 被 Redis 作者提出用于保证在集群模式下加锁的可靠性,就是去多个 Redis 节点上都尝试加锁
超过一半节点加锁成功,并且加锁后的时间要保证没有超过锁的过期时间,才算加锁成功
具体的流程比较复杂,并且性能较差,了解一下即可
所以说呢,在分布式环境下,使用 Redis 做分布式锁的话,或多或少都可能会产生一些未知的问题
并且 Redis 本质上来说也不是做分布式协调的,他只是作为一个分布式缓存解决方案存在
如果业务不追求非常高的可靠性,并且需要使用到公平锁、可重入锁、读写锁这类功能的话,可以选用 Redisson 分布式锁
2:Zookeeper分布式锁
ZooKeeper 的分布式锁的特点就是:稳定、健壮、可用性强
这得益于 ZooKeeper 这个框架本身的定位就是用来做 分布式协调 的,因此在需要保证可靠性的场景下使用 ZooKeeper 做分布式锁是比较好的
ZooKeeper 的分布式锁是 基于临时节点
来做的,多个客户端去创建临时同一个节点,第一个创建客户端抢锁成功,释放锁时只需要删除临时节点即可
但是缺点就是 ZooKeeper 分布式锁提供的功能比较少,没有读写锁、公平锁等等这些复杂的功能
因此 ZooKeeper 的分布式锁适用于 对可靠性要求很高 的业务场景,并且 需要的锁功能也比较简单
并且 ZooKeeper 的分布式锁在极端情况下也会存在不安全的问题:如果加锁的客户端长时间 GC 导致无法与 ZooKeeper 维持心跳,那么 ZK 就会认为这个客户端已经挂了,于是将该客户端创建的临时节点删除,那么当这个客户端 GC 完成之后还以为自己持有锁,但是它的锁其实已经没有了,因此也会存在不安全的问题
3:项目建议
具体选用哪一种分布式锁的话,可以根据需要使用的功能和已经引入的技术栈来进行选择
比如只用到简单的分布式锁功能,又恰好已经引入了 ZK 依赖,就可以使用 ZK 的分布式锁
其实这两种锁在真正的项目中使用的都是比较多的
而且要注意的是无论是使用 Redis 分布式锁还是 ZK 分布式锁其实在极端情况下都会出现问题,都不可以保证 100% 的安全性
可以通过分布式锁在上层互斥掉大量的请求,如果真有个别请求出现锁失效,可以在底层资源层做一些互斥保护,作为一个兜底
因此如果是对可靠性要求非常高的应用,不可以把线程安全的问题全部寄托于分布式锁,而是要在资源层也做一些保护,来保证数据真正的安全
而 Redis 中的 RedLock 尽量不使用,因为它为了保证加锁的安全牺牲掉了很多的性能,并且部署成本高(至少部署 5 个 Redis 的主库)
使用 Redis 分布式锁建议通过【主从 + 哨兵】部署集群,使用它的分布式锁