【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
**开源地址:https://docs.qq.com/doc/DSmxTbFJ1cmN1R2dB **
package com.aha.lock.zk;
import lombok.extern.slf4j.Slf4j;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
-
使用 zk 实现分布式锁
-
实现 Lock 遵循 JUC 提供的规范
-
@author: WT
-
@date: 2021/11/23 16:41
*/
@Slf4j
public class ZkDistributedLock implements Lock {
/**
-
用于协调线程的执行时机,注意:countdown 之后,再次 await 是没有办法阻塞线程的,不能使用线程隔离的变量,要不怎么实现线程之间的通知,所以 ThreadLocal 是不可行的
-
private ThreadLocal countDownLatch = ThreadLocal.withInitial(() -> new CountDownLatch(1));
*/
private CountDownLatch countDownLatch = new CountDownLatch(1);
private static final String IP_PORT = “10.211.55.3:2181”;
/**
- 根节点路径
*/
private static final String ROOT_NODE = “/LOCK”;
/**
- 当前节点的前置节点路径
*/
private ThreadLocal beforeNodePath = new ThreadLocal<>();
/**
- 当前节点路径
*/
private ThreadLocal nodePath = new ThreadLocal<>();
private ZkClient zkClient = new ZkClient(IP_PORT);
public ZkDistributedLock() {
// 创建分布式锁对象时,初始化 zk 的路径
if (!zkClient.exists(ROOT_NODE)) {
zkClient.createPersistent(ROOT_NODE);
}
}
/**
- 加锁方法
*/
@Override
public void lock() {
if (tryLock()) {
log.info(“{} 加锁成功”, Thread.currentThread().getName());
return;
}
// 阻塞 - 等待下次加锁的时机
waitForLock();
// 再次尝试加锁
lock();
}
/**
- 阻塞 - 等待下次加锁的时机
*/
private void waitForLock() {
// 监听前置节点的删除事件 - 监听内部类
IZkDataListener zkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
/**
-
监听节点的删除时间
-
@param dataPath 节点路径
-
@throws Exception 异常
*/
@Override
public void handleDataDeleted(String dataPath) throws Exception {
log.info(“{} 前置节点被删除”, dataPath);
// TODO: 2021/11/24 这个countDown 会将所有正在等待的线程都唤醒,没有实现只唤醒自己的后置节点
countDownLatch.countDown();
}
};
// 订阅监听前置节点的删除时间
zkClient.subscribeDataChanges(beforeNodePath.get(), zkDataListener);
// 判断在监听之前,前置节点是否已经被删除
if (zkClient.exists(beforeNodePath.get())) {
// 前置节点还存在 - 阻塞线程 等待前置节点被删除之后继续执行
try {
countDownLatch.await();
log.info(“阻塞线程: {}, nodePath: {}”, Thread.currentThread().getName(), nodePath.get());
} catch (InterruptedException e) {
log.info(“阻塞 {} 线程失败, 中断此线程”, Thread.currentThread().getName());
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
// 重置 countDownLatch , 前置节点已经被删除,取消订阅事件
countDownLatch = new CountDownLatch(1);
zkClient.unsubscribeDataChanges(beforeNodePath.get(), zkDataListener);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
/**
-
尝试加锁
-
@return boolean: 是否加锁成功
*/
@Override
public boolean tryLock() {
// 1. 判断 nodePath 是否为空,为空的话说明 ZkDistributedLock 第一次申请锁, zk 需要进行临时节点的创建
if (!StringUtils.hasText(nodePath.get())) {
nodePath.set(zkClient.createEphemeralSequential(ROOT_NODE + “/”, “lock”));
// log.info(“ZkDistributedLock 第一次申请锁, zk 需要进行临时节点的创建:{}”, nodePath);
log.info(“nodePath为空,创建临时节点:{}”, nodePath.get());
}
// 2. 获取 根节点所有子节点
List childrenNodeList = zkClient.getChildren(ROOT_NODE);
// 3. 将子节点列表进行排序
Collections.sort(childrenNodeList);
log.info(“nodePath: {}, nodeList:{}”, nodePath.get(), childrenNodeList);
// 4. 判断当前线程是为最小的节点,是最小的节点说明获取锁成功,反之等待并监听自己前面的节点,当自己前面的节点删除之后,就是自己再次申请锁的时候
if (nodePath.get().equals(ROOT_NODE + “/” + childrenNodeList.get(0))) {
log.info(“线程名称: {}, nodePath: {} 是最小的节点,获取锁成功。”, Thread.currentThread().getName(), nodePath.get());
return true;
} else {
// 获取当前节点应该在 节点列表中插入的位置 进而取得他的上一个节点
int i = Collections.binarySearch(childrenNodeList, nodePath.get().substring(ROOT_NODE.length() + 1));
// 获取上一个节点的路径
beforeNodePath.set(ROOT_NODE + “/” + childrenNodeList.get(i - 1));
log.info(“{} 前面的节点为:{}”, nodePath.get(), beforeNodePath.get());
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
zkClient.delete(nodePath.get());
}
@Override
public Condition newCondition() {
return null;
}
}
代码解析:
-
implements Lock
: 实现Lock
遵循JUC
提供的规范。 -
使用
zkClient.createEphemeralSequential()
创建临时顺序节点,避免羊群效应和拥有锁线程意外挂掉造成死锁的问题。 -
阻塞线程这边使用的是
CountDownLatch
。当有线程争抢到锁之后,其他的线程会被CountDownLatch
的await
方法给阻塞,当被阻塞线程的前置节点被删除,就说明当前节点应该被唤醒,因为顺序节点是有序的,所以只唤醒当前节点就可以了。这边唤醒方法使用的是zkClient.subscribeDataChanges(beforeNodePath.get(), zkDataListener);
在检测到前置节点被删除之后使用CountDownLatch
的countDown
方法,当countDown()
变成 0 之后就会唤醒线程。 -
nodePath
和beforeNodePath
应该是线程私有变量,这样才能保证,每个线程记录自己的nodePath
和beforeNodePath
。 -
具体的实现细节可以参考代码中的注释。
步骤 3 问题说明:
- 当
countDown()
变成 0 之后就会唤醒所有的线程,这边应该实现成唤醒自己下一个节点
- 当
countDwon()
变成 0 之后需要重新new
这个对象,不然await
方法是没有办法重新阻塞线程的。
测试自定义 zk
分布式锁:
package com.aha.lock.zk;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
-
@author: WT
-
@date: 2021/11/23 17:56
*/
@RestController
@Slf4j
public class ZkDistributeLockTest {
static int inventory = 10;
private static final int NUM = 10;
private final ZkDistributedLock zkDistributedLock = new ZkDistributedLock();
@GetMapping(“/zk/lock”)
public void zkLockTest() {
try {
for (int i = 0; i < NUM; i++) {
new Thread(() -> {
try {
zkDistributedLock.lock();
Thread.sleep(200);
if (inventory > 0) {
inventory–;
}
log.warn(“库存扣减完之后为:{}”, inventory);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
} finally {
zkDistributedLock.unlock();
}
}
).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用 curator 的 zk 分布式锁
curator 提供的几种分布式锁方案
-
InterProcessMutex
:分布式可重入排它锁 -
InterProcessSemaphoreMutex
:分布式排它锁 -
InterProcessReadWriteLock
:分布式读写锁
InterProcessMutex 使用实例
配置 curatorFramework
客户端
zookeeper:
address: 10.211.55.3:2181 # zookeeper Server 地址,如果有多个使用逗号分隔。如 ip1:port1,ip2:port2,ip3:port3
retryCount: 5 # 重试次数
initElapsedTimeMs: 1000 # 初始重试间隔时间
maxElapsedTimeMs: 5000 # 最大重试间隔时间
sessionTimeoutMs: 30000 # Session 超时时间
connectionTimeoutMs: 10000 # 连接超时时间
package com.aha.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
-
连接 zookeeper 配置类
-
@author: WT
-
@date: 2021/11/22 18:14
*/
@Data
@Configuration
@ConfigurationProperties(prefix = “zookeeper”)
public class ZkClientProperties {
/** 重试次数 */
private int retryCount;
/** 初始重试间隔时间 */
private int initElapsedTimeMs;
/** 最大重试间隔时间 */
private int maxElapsedTimeMs;
/**连接地址 */
private String address;
/**Session过期时间 */
private int sessionTimeoutMs;
/**连接超时时间 */
private int connectionTimeoutMs;
}
package com.aha.client;
import com.aha.config.ZkClientProperties;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
-
生成 zk 客户端
-
@author: WT