使用Zookeeper实现分布式锁的一种方法
1,锁的的基本概念
开发中锁的概念并不陌生,通过锁可以实现在多个线程或多个进程间在争抢资源时,能够合理的分配置资源的所有权。在单体应用中我们可以通过 synchronized 或ReentrantLock 来实现锁。但在分布式系统中,仅仅是加synchronized 是不够的,需要借助第三组件来实现。比如一些简单的做法是使用 关系型数据行级锁来实现不同进程之间的互斥,但大型分布式系统的性能瓶颈往往集中在数据库操作上。为了提高性能得采用如Redis、Zookeeper之内的组件实现分布式锁。
- 共享锁 也称作只读锁,当一方获得共享锁之后,其它方也可以获得共享锁。但其只允许读取。在共享锁全部释放之前,其它方不能获得写锁;
2.排他锁 也称作读写锁,获得排它锁后,可以进行数据的读写。在其释放之前,其它方不能获得任何锁。
2,基于zk实现分布式锁的原理图
3,主要代码示例
主要代码如下:
.
// Lock实体类
package com.opensoul.zk.lock;
import java.io.Serializable;
/**
* @Description: 锁节点实例类
* @Author wgxu
* @date 2020/10/18 12:12
*/
public class Lock implements Serializable {
private static final long serialVersionUID = 1908357942011709844L;
private String lockId;
private String path;
private boolean isActive;
@Override
public String toString() {
return "Lock{" +
"lockId='" + lockId + '\'' +
", path='" + path + '\'' +
", isActive=" + isActive +
'}';
}
public Lock(String lockId, String path) {
this.lockId = lockId;
this.path = path;
}
public String getLockId() {
return lockId;
}
public void setLockId(String lockId) {
this.lockId = lockId;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isActive() {
return isActive;
}
public void setActive(boolean active) {
isActive = active;
}
}
// An highlighted block
package com.opensoul.zk.lock;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* @Description: 锁的逻辑实现类
* @Author wgxu
* @date 2020/10/18 12:12
*/
public class ZookeeperLock implements Serializable {
private static final Logger logger = Logger.getLogger(ZookeeperLock.class);
private static final long serialVersionUID = 635945547698890884L;
//zk服务器参数
private String server = "127.0.0.1:2181";
private ZkClient zkClient;
//固定持久节点
private static final String rootPath = "/zk-lock";
public ZookeeperLock() {
logger.error("准备建立连接....................");
zkClient = new ZkClient(server, 5000, 30000);
logger.error("已经和服务器建立连接,准备创建根节点");
buildRoot();
}
// 构建根节点
public void buildRoot() {
if (!zkClient.exists(rootPath)) {
zkClient.createPersistent(rootPath);
}
}
// 获取锁
public Lock lock(String lockId, long timeout) {
// 创建临时节点
Lock lockNode = createLockNode(lockId);
logger.debug("------"+Thread.currentThread().getId() + "------00000000号位创建临时节点:" + lockNode.getPath());
lockNode = tryActiveLock(lockNode);// 尝试激活锁
if (!lockNode.isActive()) {
try {
synchronized (lockNode) {
lockNode.wait(timeout); // 线程锁住
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (!lockNode.isActive()) {
throw new RuntimeException(" lock timeout");
}
return lockNode;
}
// 释放锁
public void unlock(Lock lock) {
if (lock.isActive()) {
zkClient.delete(lock.getPath());
}
}
// 尝试激活锁
private Lock tryActiveLock(Lock lockNode) {
logger.debug("------"+Thread.currentThread().getId() + "------11111111号位试图激活节点:" + lockNode.getPath());
// 获取根节点下面所有的子节点
List<String> list = zkClient.getChildren(rootPath)
.stream()
.sorted()
.map(p -> rootPath + "/" + p)
.collect(Collectors.toList()); // 判断当前是否为最小节点
logger.debug("------"+Thread.currentThread().getId() + "------22222222号位获得root下的所有子节点:" + list);
String firstNodePath = list.get(0);
logger.debug("------"+Thread.currentThread().getId() + "------33333333号位获得root下的所有子节点中的最小节点:[" + lockNode.getPath() + "]");
// 最小节点是不是当前节点
if (firstNodePath.equals(lockNode.getPath())) {
logger.debug("------"+Thread.currentThread().getId() + "------44444444号位获得root下的所有子节点中的最小节点:[" + firstNodePath + "] 位本节点自身,设置激活状态为[" + lockNode.getPath().equals(firstNodePath) + "]");
lockNode.setActive(true);
} else {
String upNodePath = list.get(list.indexOf(lockNode.getPath()) - 1);
logger.debug("------"+Thread.currentThread().getId() + "------55555555号位,当前节点与最小节点不一样,获取当前节点:[" + lockNode.getPath() + "]的上一个节点[" + upNodePath + "]");
logger.debug("------"+Thread.currentThread().getId() + "------555555-1号位,当前节点与最小节点不一样,获取当前节点:[" + lockNode.getPath() + "]的上一个节点[" + upNodePath + "] 进行监听......");
zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
// 事件处理 与心跳 在同一个线程,如果Debug时占用太多时间,将导致本节点被删除,从而影响锁逻辑。
logger.debug("------"+Thread.currentThread().getId() + "------555555-2号位,当前节点:[" + lockNode.getPath() + "]的上一个节点[" + upNodePath + "] 被监听到已经被删除了!");
logger.debug("节点删除:" + dataPath);
logger.debug("------"+Thread.currentThread().getId() + "------555555-3号位,再次试图激活当前节点:[" + lockNode.getPath() + "]");
Lock lock = tryActiveLock(lockNode);
logger.debug("------"+Thread.currentThread().getId() + "------555555-4号位,再次试图激活当前节点:[" + lockNode.getPath() + "]后,节点被激活");
synchronized (lockNode) {
logger.debug("------"+Thread.currentThread().getId() + "------555555-5号位,synchronized当前节点:[" + lockNode.getPath() + "]判断节点是否已经被激活["+lock.isActive()+"]");
if (lock.isActive()) {
logger.debug("------"+Thread.currentThread().getId() + "------555555-6号位,synchronized当前节点:[" + lockNode.getPath() + "]判断节点是否已经被激活["+lock.isActive()+"],激活完成,唤醒等待notify......");
lockNode.notify(); // 释放了
}
}
logger.debug("------"+Thread.currentThread().getId() + "------555555-7号位,解除监听当前节点:[" + lockNode.getPath() + "]的上一个个["+lock.isActive()+"],激活完成,唤醒等待notify......");
zkClient.unsubscribeDataChanges(upNodePath, this);//解除监听
}
});
}
logger.debug("------"+Thread.currentThread().getId() + "------4444444-1号位获得root下的所有子节点中的最小节点:[" + firstNodePath + "] 位本节点自身," +
"设置激活状态为[" + lockNode.getPath().equals(firstNodePath) + "],获得锁后返回。return lockNode");
return lockNode;
}
public Lock createLockNode(String lockId) {
String nodePath = zkClient.createEphemeralSequential(rootPath + "/" + lockId, "w");
return new Lock(lockId, nodePath);
}
public ZkClient getZkClient() {
return zkClient;
}
}
测试类:
.
package com.opensoal.zk.test;
import com.opensoul.zk.lock.Lock;
import com.opensoul.zk.lock.ZookeeperLock;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class LockTest {
private static final Logger logger = Logger.getLogger(LockTest.class);
ZookeeperLock zookeeperLock;
static Long count = 0L;
@Before
public void init() {
zookeeperLock = new ZookeeperLock();
}
/**
* 模拟测试竞争锁
* @throws InterruptedException
* @throws IOException
*/
@Test
public void run() throws InterruptedException, IOException {
// 写数字 0+100 =100
File file = new File("e:/test.txt");
if (!file.exists()) {
file.createNewFile();
}
ExecutorService executorService = Executors.newCachedThreadPool();
logger.info(file.getPath());
for (int i = 0; i < 10; i++) {// 1000 个线程存在问题
executorService.submit(() -> {
//d:\test.txt
Lock lock = zookeeperLock.lock(file.getPath(), 60 * 1000);
logger.info(lock.getPath()+"------------------------------被激活获得锁");
try {
String firstLine = Files.lines(file.toPath()).findFirst().orElse("0");
int count = Integer.parseInt(firstLine);
count++;
Files.write(file.toPath(), String.valueOf(count).getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
zookeeperLock.unlock(lock);
}
});
}
executorService.shutdown();
executorService.awaitTermination(200, TimeUnit.SECONDS);
String firstLine = Files.lines(file.toPath()).findFirst().orElse("0");
logger.info(firstLine);
}
/**
* 测试获取锁
* @throws InterruptedException
*/
@Test
public void getLockTest() throws InterruptedException {
zookeeperLock.createLockNode("E:\test.txt");
Lock lock = zookeeperLock.lock("opensoul", 60 * 1000);
logger.info("------"+Thread.currentThread().getId() + "------成功获取锁");
Thread.sleep(Long.MAX_VALUE);
assert lock != null;
}
}
4,项目源码git地址:
https://gitee.com/ygbccqzxxdr/zookeeper-lock.git代码如有错误之处,敬请指正。
我假装老成,人们就传我老成; 我假装是个懒汉,人们就谣传我是个懒惰虫; 我充阔,人们都以为我是阔佬; 我故作冷淡,人们就说我是个无情的家伙; 然而当我真的难过万分,不禁发出痛苦呻吟的时候,人们却认为我无病呻吟。 世上本就不存在感同身受,笑容可以瞒过别人,心痛却瞒不过自己。