Zookeeper实现通知式分布式锁及Zookeeper事件监听机制的坑
凌麟柒
这里写自定义目录标题
用Zookeeper原生API实现分布式锁
与Redis分布式锁的对比:
Zookeeper监听机制的知识点:
zookeeper机制的特点
ZooKeeper对Watch提供了什么保障
Zookeeper监听事件参考
首先,由下面这篇文章代码的示例,编写出Zookeeper分布式锁的基本骨架。
用Zookeeper原生API实现分布式锁
原文代码链接:https://mp.weixin.qq.com/s/98J7I5RwTXdMGMyPlHV__Q
加入Zookeeper依赖
<!-- Zookeeper依赖 -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
</dependency>
ZookeeperSession会话类
package cn.kiring.zookeeperdemo.distributedlock;
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Zookeeper会话类
* @author kiring
* @date 2020/5/31 17:51
*/
@Slf4j
public class ZookeeperSession {
/**
* 连接信号量控制器(作用于初始化,控制zookeeper连接对象完全初始化)
*/
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
/**
* Zookeeper连接对象
*/
private ZooKeeper zooKeeper;
/**
* 本地连接控制
*/
// private CountDownLatch latch; 注解:①
private ConcurrentLinkedQueue<CountDownLatch> concurrentLinkedQueue = new ConcurrentLinkedQueue();
/**
* 第一个参数是集群地址,由构造方法注释可知,2181可以不加
* 第二个参数是 session timeout in milliseconds 这里代表的是客户端和服务端的连接超时时间,单位是毫秒。这里并不是意味着只能连接50秒,而是说连接中断,或者心跳检测不到,50s后才会断开连接
* (这里的意思是,如果关闭客户端,是不会马上删除临时节点的,而是等到心跳检测到客户端关闭了才会删除临时节点)
* 第三个参数是Zookeeper监听器,用来监听zookeeper事件的发生
*/
public ZookeeperSession(){
try {
// 防止反射对单例的破坏
if(ZookeeperSessionSingleton.instance != null){
throw new RuntimeException("单例对象不允许创建多个实例");
}
zooKeeper = new ZooKeeper("192.168.116.131:2181,192.168.116.132:2181,192.168.116.133:2181",5000,new ZookeeperWatcher());
try {
connectedSemaphore.await(); // 注解:②
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("ZooKeeper session established......");
} catch (IOException e){
log.error("初始化zooKeeper出错,发生IO异常");
e.printStackTrace();
}
}
/**
* 获取ZookeeperSession实体
* @return ZookeeperSession单例对象
*/
public static ZookeeperSession getInstance(){
return ZookeeperSessionSingleton.instance;
}
/**
* 静态内部类获取单例对象
*/
private static class ZookeeperSessionSingleton{
private static ZookeeperSession instance = new ZookeeperSession();
}
/**
* 自实现Zookeeper监听器
*/
private class ZookeeperWatcher implements Watcher {
@Override
public void process(WatchedEvent event) {
System.out.println("[" + Thread.currentThread().getName() + "] Receive watched eventState:" + event.getState() + ", eventType: " + event.getType());
if(KeeperState.SyncConnected == event.getState()){
// 触发完成zookeeper对象的初始化
connectedSemaphore.countDown();
}
// 所有事件,唤醒阻塞线程争夺锁
CountDownLatch latch = concurrentLinkedQueue.poll();
System.out.println("[" + Thread.currentThread().getName() +"] get CountDownLatch from concurrentQueue:" + latch);
if (latch != null){
// 这里线程不安全,如果一个新的线程把该
latch.countDown();
}
}
/**
* 获取分布式锁
*
* @param productId
*/
public Boolean acquireDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
// Ids.OPEN_ACL_UNSAFE 创建开放节点,允许任意操作
// CreateMode.EPHEMERAL 同步创建临时节点会话关闭后它会自动被删除
zooKeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("[" + Thread.currentThread().getName() +"] acquire the lock for product[id=" + productId + "]......");
return true;
} catch (Exception e) {
while (true) {
try {
// 相当于是给node注册一个监听器,去看看这个监听器是否存在
// true使用watcher,这里的watcher是在创建 ZooKeeper实例时指定的 watcher,
// 需要注意的是,每个watcher只能用一次,所以这里每次都会一个新的watcher
Stat stat = zooKeeper.exists(path, true);
if (stat != null) {
System.out.println("[" + Thread.currentThread().getName() +"] stat is not null");
CountDownLatch latch = new CountDownLatch(1);
System.out.println("[" + Thread.currentThread().getName() +"] put CountDownLatch in the concurrentQueue:" + latch);
concurrentLinkedQueue.add(latch);
boolean await = latch.await(5000, TimeUnit.MILLISECONDS); // 注解:③
System.out.println("[" + Thread.currentThread().getName() +"] thread is wakeup" + ", await:" + await);
if(!await){
return false;
}
}
zooKeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
// 再次触发监听(监听下一次的删除节点事件)
zooKeeper.getData(path,true,null); // 注解:④
System.out.println("[" + Thread.currentThread().getName() +"] acquire the lock for product[id=" + productId + "]......");
return true;
} catch (Exception ee) {
ee.printStackTrace();
continue;
}
}
}
}
/**
* 释放掉一个分布式锁
*
* @param productId
*/
public void releaseDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
// -1参数代表不在意版本号都删除
zooKeeper.delete(path, -1);
System.out.println("[" + Thread.currentThread().getName() +"] release the lock for product[id=" + productId + "]......");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 防止反序列化对单例的破坏
* @return 单例对象
* @throws ObjectStreamException
*/
Object readResolve() throws ObjectStreamException {
return ZookeeperSessionSingleton.instance;
}
}
下面对3个 “注解” 进行解析
注解①:
在代码中,我注释掉了private CountDownLatch 这一成员变量,因为在原本的示例中,作者是直接在每个加锁阻塞的逻辑中,设置这个成员变量的,这是线程不安全的。仅仅出于线程不安全的考虑,我将它注释了,并且使用了ConcurrentLinkedQueue来替换,目的就是想让每个线程都可以被唤醒。这里如果了解了Zookeeper的监听机制,就发现其实是不需要的,就按原作者的思路也是可以的。
注解②:
在初始化Zookeeper连接的时候,需要阻塞主线程,等到zookeeper响应了连接事件后,才唤醒主线程,完成初始化,这是因为在没有正确的连接前,都可能获得一个未完成的zookeeper连接对象。(尽管在Spring中这个发生的概率很小,因为Spring加载bean也需要时间)
注解③:
在判断存在了节点时,就需要阻塞当前线程,并且需要将当前线程的CountDownLatch提前存进队列中,后续获得锁时可以唤醒线程。并且需要注意的是,zookeeper.exists(path,true)的方法,设置为ture时,也同时会对这个节点注册一个新的监听器,监听器默认使用构造函数的那段代码创建。
注解④:
在创建完一个节点之后,紧接着就是对这个节点再做一个监听(zooKeeper.getData(path,true,null)是获取节点的信息,但是同时也会注册监听器),这是因为Zookeeper的事件监听是对于同一个客户端的同一个节点只会发送一次节点事件,这也就意味着,之前多个线程通过zookeeper.exists(path,true)注册的事件,在一次事件发生前,也仅会响应一次(经过代码试验,已验证),这样就导致释放锁了,但是大量线程都阻塞着,不会重新获取锁,而浪费CPU资源。之所以说原作者的思路也是可以的,因为他并不是像我一样超时等待后就直接返回false,而是继续循环尝试获取锁,又因为这是分布式加锁,没有谁先谁后的概念,所以也是可以的。不过原作者的这种方式相应地也会更加消耗CPU资源。
这里也可以不用事件监听的方式,事件监听主要是为了能够唤醒线程重新去获得锁,我们可以直接从ConcurrentLinkedQueue队列中拿到CountDownLatch让线程往下执行即可。不过同理,因为不是事件触发
下面写一段代码测试Zookeeper实现分布式锁的性能:
public class ZookeeperLockTest {
public static final int thread_num = 50;
public static AtomicInteger success = new AtomicInteger();
public static AtomicInteger failed = new AtomicInteger();
public CountDownLatch countDownLatch = new CountDownLatch(thread_num);
public static void main(String[] args) throws Exception{
ZookeeperSession zookeeperSession = ZookeeperSession.getInstance();
CountDownLatch countDownLatch = new CountDownLatch(thread_num);
for (int i = 0; i < thread_num; i++) {
new Thread(() -> {
Long productId = new Random().nextLong()%2==0 ? 1L : 2L;
Boolean aBoolean = zookeeperSession.acquireDistributedLock(productId);
if(aBoolean){
success.incrementAndGet();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
zookeeperSession.releaseDistributedLock(productId);
} else {
failed.incrementAndGet();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("=============成功数:" + success.get());
System.out.println("=============失败数:" + failed.get());
new Scanner(System.in).next();
}
}
测试环境:
1.三台512M内存的CentOS7虚拟机搭建的zookeeper集群
2. 宿主机是6核8G windows10
3. JDK1.8
由上面的测试代码,50个线程,分别对productId为1和productId为2的节点加锁,假如每个线程执行需要200ms,并且等待锁超时是5000ms,那么结果如下:
=============成功数:24
=============失败数:26
与Redis分布式锁的对比:
Redis分布式锁更适合拿来做重复调用的控制。如避免重复下单,避免重复绑定等。而如果要做等待锁且重试,不如Zookeeper好用,因为Zookeeper有节点动态的事件通知,配合这个可以很方便实现Java线程间的通知。
这里是使用原生Zookeeper API实现分布式锁,可以直接并且推荐直接采用Curator封装ZookeeperAPI的开源框架去完成。
Zookeeper监听机制的知识点:
以下是原文:https://blog.csdn.net/wo541075754/article/details/70207722的引用
zookeeper机制的特点
zookeeper的getData(),getChildren()和exists()方法都可以注册watcher监听。而监听有以下几个特性:
一次性触发(one-time trigger)
当数据改变的时候,那么一个Watch事件会产生并且被发送到客户端中。但是客户端只会收到一次这样的通知,如果以后这个数据再次发生改变的时候,之前设置Watch的客户端将不会再次收到改变的通知,因为Watch机制规定了它是一个一次性的触发器。
当设置监视的数据发生改变时,该监视事件会被发送到客户端,例如,如果客户端调用了 getData(“/znode1”, true) 并且稍后/znode1 节点上的数据发生了改变或者被删除了,客户端将会获取到 /znode1 发生变化的监视事件,而如果 /znode1再一次发生了变化,除非客户端再次对 /znode1 设置监视,否则客户端不会收到事件通知。
发送给客户端(Sent to the client)
这个表明了Watch的通知事件是从服务器发送给客户端的,是异步的,这就表明不同的客户端收到的Watch的时间可能不同,但是ZooKeeper有保证:当一个客户端在看到Watch事件之前是不会看到结点数据的变化的。例如:A=3,此时在上面设置了一次Watch,如果A突然变成4了,那么客户端会先收到Watch事件的通知,然后才会看到A=4。Zookeeper 客户端和服务端是通过 Socket进行通信的,由于网络存在故障,所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的,Zookeeper
本身提供了保序性(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的 znode发生了变化(a client will never see a change for which it has set a watch until it first sees the watch event)。网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序。
被设置了watch的数据(The data for which the watch was set)
这是指节点发生变动的不同方式。你可以认为ZooKeeper维护了两个watch列表:data watch和child watch。getData()和exists()设置data watch,而getChildren()设置child watch。或者,可以认为watch是根据返回值设置的。getData()和exists()返回节点本身的信息,而getChildren()返回子节点的列表。因此,setData()会触发znode上设置的data watch(如果set成功的话)。一个成功的 create() 操作会触发被创建的znode上的数据watch,以及其父节点上的child watch。而一个成功的 delete()操作将会同时触发一个znode的data watch和child watch(因为这样就没有子节点了),同时也会触发其父节点的child watch。Watch由client连接上的ZooKeeper服务器在本地维护。这样可以减小设置、维护和分发watch的开销。当一个客户端连接到一个新的服务器上时,watch将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到watch的。而当client重新连接时,如果需要的话,所有先前注册过的watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch可能会丢失:对于一个未创建的znode的exist
watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个watch事件可能会被丢失。
ZooKeeper对Watch提供了什么保障
对于watch,ZooKeeper提供了这些保障:
Watch与其他事件、其他watch以及异步回复都是有序的。 ZooKeeper客户端库保证所有事件都会按顺序分发。 客户端会保障它在看到相应的znode的新数据之前接收到watch事件。//这保证了在process()再次利用zk client访问时数据是存在的 从ZooKeeper接收到的watch事件顺序一定和ZooKeeper服务所看到的事件顺序是一致的。
关于Watch的一些值得注意的事情
Watch是一次性触发器,如果得到了一个watch事件,而希望在以后发生变更时继续得到通知,应该再设置一个watch。
因为watch是一次性触发器,而获得事件再发送一个新的设置watch的请求这一过程会有延时,所以无法确保看到了所有发生在ZooKeeper上的
一个节点上的事件。所以请处理好在这个时间窗口中可能会发生多次znode变更的这种情况。(可以不处理,但至少要意识到这一点)。//也就是说,在process()中如果处理得慢而没有注册new watch时,在这期间有其它事件出现时是不会通知!!
一个watch对象或一个函数/上下文对,为一个事件只会被通知一次。比如,如果同一个watch对象在同一个文件上分别通过exists和getData注册了两次,而这个文件之后被删除了,这时这个watch对象将只会收到一次该文件的deletion通知。//同一个watch注册同一个节点多次只会生成一个event。
当从一个服务器上断开时(比如服务器出故障了),在再次连接上之前,将无法获得任何watch。请使用这些会话事件来进入安全模式:在disconnected状态下将不会收到事件,所以程序在此期间应该谨慎行事。
Zookeeper监听事件参考
这里因为笔者没有去系统的学习Zookeeper,所以事件的监听总结可以直接参考其他博主的文章:https://blog.csdn.net/liu857279611/article/details/70495413
参考:
《实现分布式锁都有哪些方式?》
《Zookeeper之Watcher监听事件丢失分析》
————————————————
版权声明:本文为CSDN博主「凌麟柒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40233503/article/details/106489207