文章目录
参考蚂蚁课堂
1.Zookeeper实现事件监听通知
zookeeper可以实现对节点不同行为进行监听比如说之前payment-service下面有两个节点一个是8080一个是8081。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k9VWIowr-1649339739710)(C:\Users\MICROSOFT\Pictures\博客图片\zookeeper\服务注册与发现\注册成功节点.png)]
我对8080进行监听,然后我把8080删除然后控制台就会提醒我8080被删了。这就是对事件的监听。下面我们来看一下实现的代码和最后的结果。
public class Test006 {
//参数1 连接地址
private static final String ADDRES = "192.168.247.3:2181";
// 参数2 zk超时时间
private static final int TIMEOUT = 5000;
// 计数器
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) {
// 创建了一个父节点 /mayikt-service/8080|8081
// 1.创建我们的zk连接
ZkClient zkClient = new ZkClient(ADDRES, TIMEOUT);
String parentPath = "/payment-service";
// 2.监听节点的value值是否发生变化
zkClient.subscribeDataChanges(parentPath + "/8080", new IZkDataListener() {
@Override
// 节点的内容是否发生变化
public void handleDataChange(String s, Object o) throws Exception {
System.out.println("s:" + s + ",o:" + o);
}
@Override
// 监听该节点是否被删除
public void handleDataDeleted(String s) throws Exception {
System.out.println("s被删除:" + s);
}
});
while (true) {
}
}
}
首先我们先建立zkClient指定好,IP地址和端口号,然后指明要监听哪个节点这里是payment-service下的8080节点。然后我们删除这个节点最后结果如下图所示。
2.分布式锁的概念
2.1分布式锁应用场景
假设有这样一个场景比如说有一个业务逻辑需要定时执行, 我们就可以弄一个定时任务调度,定时执行该业务逻辑,但是假如说我要部署一个集群,也就是两台服务器jar包代码业务逻辑都是一样的,那么在同一时刻这个业务逻辑可能被执行两遍,这完全没有必要,有的时候还可能出现问题,所以我们就可以用分布式锁来对他们进行限制,保证同一时刻只有一台服务器可以拿到锁执行业务逻辑,其他服务器没拿到锁进入阻塞。分布式锁是相对于JVM的,保证在多个JVM中只能有一个JVM执行。平时说的锁比如说lock是相对于线程的。
2.2实现方案
1.基于数据库实现分布式锁
2.基于Redis实现分布式锁
3.基于zk实现分布式锁
4.基于redisson实现分布式锁
2.3基于zookeeper实现分布式锁的思路
2.3.1创建分布式锁的原理
1.多个Jvm同时在Zookeeper上创建相同的临时节点(lockPath)。
2.因为临时节点路径保证唯一性,只要谁能够创建成功谁就能够获取锁,就可以开始执行业务逻辑;同时只要这个会话结束就节点就会消失,这样下一轮其他Jvm也有获取锁的机会。
3.如果节点已经给其他请求创建的话,当前的请求实现等待。
2.3.2释放锁的原理
因为我们采用临时节点,当前节点成功,表示获取锁成功;正常执行完业务逻辑之后调用Session关闭连接方法,当前节点会删除。其他正在等待请求的Jvm采用事件监听,如果当前节点被删除的话,又重新进入到获取锁的流程;
综上所述:临时节点+事件通知 = zk分布式锁。
3. zk分布式锁的实现
首先我们可以将失信分布式锁的一些必要的步骤抽象出来,这样的话我们就不仅可以用zookeeper来实现,也可以用其他的比如redis,数据库之类的来实现分布式锁。
public interface Lock {
/**
* 获取锁
*/
public void getLock();
/**
* 释放锁
*/
public void unLock();
}
abstract class AbstractTemplzateLock implements Lock {
@Override
public void getLock() {
// 模版方法 定义共同抽象的骨架
if (tryLock()) {
System.out.println(">>>" + Thread.currentThread().getName() + ",获取锁成功");
} else {
// 开始实现等待
waitLock();// 事件监听
// 重新获取
getLock();
}
}
/**
* 获取锁
*
* @return
*/
protected abstract boolean tryLock();
/**
* 等待锁
*
* @return
*/
protected abstract void waitLock();
/**
* 释放锁
*
* @return
*/
protected abstract void unImplLock();
@Override
public void unLock() {
unImplLock();
}
}
首先来一个Lock接口,里面有两个方法,也就是上面实现分布式锁思路中提到的获取锁和释放锁。然后使用一个抽象类来实现上述接口,首先是获取锁getLock(),这个获取锁有两种可能一种是获取锁成功,另一种是获取锁失败,如果获取锁失败的话,那么它将不断的等待,这个等待方法即waitLock(),这个方法实际上是一个事件监听,当zookeeper节点删除之后,waitLock()就会监听到,监听到了之后就认为上一个Jvm已经将分布式锁释放,所以我这个Jvm就会执行下面的getLock()方法重新获取锁。然后我们看看具体的zookeeper是如何实现这些方法的。
public class ZkTemplateImplLock extends AbstractTemplzateLock {
//参数1 连接地址
private static final String ADDRES = "192.168.247.3:2181";
// 参数2 zk超时时间
private static final int TIMEOUT = 5000;
// 创建我们的zk连接
private ZkClient zkClient = new ZkClient(ADDRES, TIMEOUT);
/**
* 共同的创建临时节点
*/
private String lockPath = "/lockPath";
private CountDownLatch countDownLatch = null;
@Override
protected boolean tryLock() {
// 获取锁的思想:多个jvm同时创建临时节点,只要谁能够创建成功 谁能够获取到锁
try {
zkClient.createEphemeral(lockPath);
return true;
} catch (Exception e) {
// // 如果创建已经存在的话
// e.printStackTrace();
return false;
}
}
@Override
protected void waitLock() {
// 1.使用事件监听 监听lockPath节点是否已经被删除,如果被删除的情况下 有可以重新的进入到获取锁的权限
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
if (countDownLatch != null) {
countDownLatch.countDown();// 计数器变为0
}
}
};
zkClient.subscribeDataChanges(lockPath, iZkDataListener);
// 2.使用countDownLatch等待
if (countDownLatch == null) {
countDownLatch = new CountDownLatch(1);
}
try {
countDownLatch.await();// 如果当前计数器不是为0 就一直等待
} catch (Exception e) {
}
// 3. 如果当前节点被删除的情况下,有需要重新进入到获取锁
}
@Override
protected void unImplLock() {
if (zkClient != null) {
zkClient.close();
System.out.println(Thread.currentThread().getName() + ",释放了锁>>>");
}
}
}
这个zkTemplateLock继承了上面的抽象类,然后重写了tryLock()方法,这个tryLock方法就是首先我创建一个临时节点,如果能创建成功返回true,如果失败则会抛出异常,返回false。然后就是下面的waitLock,waitLock是等待重新获取锁,它还包含了事件监听功能,我们通过countDownLatch来让他达到阻塞状态,当countDownLatch为空的时候创建一个countDownLatch默认值为1,如果之前节点没有被删除,也就是没释放锁的话countDownLatch一直就是1,也就是说他会一直阻塞,当那个节点被删除时,他会监听到然后将countDownLatch.countDown()减一然后就不会被阻塞,最终会退出waitLock方法,然后重新执行getLock()。释放锁就是把zk连接关了临时节点自动消失。
然后我们来做个测试。这里面我们用多线程去模拟多个Jvm,我们可以写一个订单Service类。
public class OrderService implements Runnable {
private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
private Lock lock = new ZkTemplzateImplLock();
@Override
public void run() {
getNumber();
}
private void getNumber() {
try {
lock.getLock();
Thread.sleep(50);
String number = orderNumGenerator.getNumber();
System.out.println(Thread.currentThread().getName() + ",获取的number:" + number);
// 如果zk超时了,有做数据库写的操作统一直接回滚
} catch (Exception e) {
} finally {
lock.unLock();
}
}
}
OrderService实现了Runnable接口,一会我们开多个线程让这个OrderService生成订单号,然后重写他的run方法,这个run方法里面是getNumber生成订单号,然后这个订单号由这个类来实现。
public class OrderNumGenerator {
/**
* 序号
*/
private static int count;
/**
* 生成我们的时间戳 为订单号码
*
* @return
*/
public String getNumber() {
SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-OHH-mm-ss");
try {
Thread.sleep(30);
} catch (Exception e) {
}
return simpt.format(new Date()) + "-" + ++count;
}
}
实际上就是一个时间戳加上count计数器。在OrderService当中,getNumber这个函数里面首先要获取分布式锁,如果获取成功才会执行下面的生成订单号的业务逻辑。然后最终释放锁。我们开启Zookeeper来看一下运行结果。
我们能看到是我们想要的结果,每个线程都会获取锁然后执行业务逻辑打印出订单号,然后释放锁,紧接着下一个线程进来执行同样的流程。
4.如何防止死锁问题
假如说出现了这样一个场景,在执行业务逻辑的过程当中,业务逻辑是一个非常耗时的逻辑,这时候有可能拿到锁了,一直不释放锁,这就造成了死锁。我们可以这样解决设置一个超时时间达到一定的时间,直接释放zk连接,然后我们将数据库发生写操作的部分全部回滚。