吐血推荐——详解分布式锁

【一线大厂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;

}

}

代码解析:

  1. implements Lock: 实现 Lock 遵循 JUC 提供的规范。

  2. 使用 zkClient.createEphemeralSequential() 创建临时顺序节点,避免羊群效应和拥有锁线程意外挂掉造成死锁的问题。

  3. 阻塞线程这边使用的是 CountDownLatch。当有线程争抢到锁之后,其他的线程会被 CountDownLatchawait 方法给阻塞,当被阻塞线程的前置节点被删除,就说明当前节点应该被唤醒,因为顺序节点是有序的,所以只唤醒当前节点就可以了。这边唤醒方法使用的是 zkClient.subscribeDataChanges(beforeNodePath.get(), zkDataListener); 在检测到前置节点被删除之后使用 CountDownLatchcountDown 方法,当 countDown() 变成 0 之后就会唤醒线程。

  4. nodePathbeforeNodePath 应该是线程私有变量,这样才能保证,每个线程记录自己的 nodePathbeforeNodePath

  5. 具体的实现细节可以参考代码中的注释。

步骤 3 问题说明:

  1. countDown() 变成 0 之后就会唤醒所有的线程,这边应该实现成唤醒自己下一个节点
  1. 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 提供的几种分布式锁方案

  1. InterProcessMutex:分布式可重入排它锁

  2. InterProcessSemaphoreMutex:分布式排它锁

  3. 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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值