基于Zookeeper使用ZkClient实现分布式锁

2 篇文章 0 订阅
2 篇文章 0 订阅

有段时间没写博客了,在整理之前写过的一套自定义框架,并且整理好上传值github上了,也有一些新功能还在开发,欢迎大家使用:一个好用的Http接口请求工具组件

可能今天这篇文章跟之前的比有些跳跃性,一下子就谈到了Zookeeper了,不过也没关系啦,先谈谈最常用,然后在慢慢看Zooeeper的其他知识。

简单介绍

ZooKeeper致力于提供一个高性能、高可用,且具备严格的顺序访问控制能力的分布式协调服务,是雅虎公司创建,是GoogleChubby一个开源的实现,也是HadoopHbase的重要组件。它的数据以树形结构(类似于文件系统)储存在内存当中由于数储存在内存当中(并且每个节点都必须要有数据),所以拿取数据效率特别快。

分布式锁:功能与之前并发编程中的Lock功能一致,主要是为了解决共享资源被竞争所导致的并发问题。由于并发编程当中锁是在当前的JVM当中,而对于分布式的服务来说单纯的JVM的锁已经不起作用了,不过实现功能还是一致。

监听机制

既然是锁,那么就存在线程等待以及线程被唤醒功能,所以就需要有一个监听机制,当ZooKeeper上的锁被释放之后需监听到,并且通知服务去获取锁资源,正好在ZooKeeper当中存在一种监听机制,为事件监听器(Watcher)

事件监听器:客户端可以在节点上注册监听器,当特定的事件发生后,ZooKeeper会通知到感兴趣的客户端;被监听的事件有:NodeCreated(节点创建)、NodeDeleted(节点删除)NodeDataChanged(节点数据被改变)NodeChildrenChange(子节点被修改)

Node类型

基于ZooKeeper分布式锁必然基于节点,在ZooKeeper创建节点共有四点类型:

1、持久化节点(PERSISENT): 同一个节点路径只能创建一个节点,并且连接创建节点后,断开连接后,节点仍然存在并且关闭服务会保存至磁盘上。

2、持久化顺序节点(PERSISENT_SEQUENTIAL):同一个节点路径可以创建多个节点,并且ZooKeeper会自动分配一个按顺序的节点号,断开连接后,节点仍然会保存至磁盘。

3、临时节点(EPHEMERAL):在一个连接中,同一路径下只能创建一个节点,当创建节点的连接关闭后,该节点会被删除,如果非正常关闭连接,则过一段时间后节点会被删除。

4、临时顺序节点(EPHEMERAL_SEQUENTIAL):同一路径下可以创建多个节点,但是节点名称ZooKeeper会自动分配一个按顺序的节点号,当连接关闭后,这些节点会被删除。

实现方式

先引入一下zkClient的坐标

<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>

既然是锁,那么必定是同一个节点,而且要先去尝试获取到锁,如果没有获取到,那么就进入等待,并且监听同一个节点是否被删除(或者修改),如果删除,则唤醒等待的线程,并且再次去获取ZooKeeper上的锁节点;那么整体的流程如下:

//锁节点
public static final String PATH = "/lock";
//尝试获取锁	
public abstract boolean tryLock();
//等待锁释放
public abstract void waitLock();
//释放锁
public abstract void unLock();
//获取锁
void getLock(){
	if(tryLock()) {
        //获取到锁后,进行业务操作
		System.out.println(Thread.currentThread().getName() + " get Lock");
	}else {
        //没有获取则进入等待,并且监听锁节点是否被释放
		waitLock();
		//再次获取锁
        getLock();
	}
}

 我这里是采用ZkClient进行操作ZooKeeper的,先创建一个ZkClient连接:

	CountDownLatch latch = null;
	
	private static final String CONNECTION = "127.0.0.1:2181";
	
	ZkClient zkClient = new ZkClient(CONNECTION,3000);

先来看看尝试获取锁可以怎样实现:

@Override
public boolean tryLock() {
	try {
        //创建临时节点,也可以创建持久化节点,到时候释放节点的时候删除就好了
		zkClient.createEphemeral(PATH, "1".getBytes());
		return true;
	} catch (Exception e) {
        //如果创建失败,则获取节点锁失败,则进入等待
		return false;
	}
}

进入等待,并且监听锁节点是否删除或者修改:

@Override
public void waitLock() {
	//创建监听事件
	IZkDataListener listen = new IZkDataListener() {

		public void handleDataChange(String dataPath, Object data) throws Exception {
			//当前方法为监听节点修改,如果节点进行修改,那么就会执行当前方法
			
		}

		public void handleDataDeleted(String dataPath) throws Exception {
			//我这里是释放锁为删除节点,删除会执行当前方法
			latch.countDown();
		}
		
	};
	//注册监听器
	zkClient.subscribeDataChanges(PATH, listen);
	//如果ZooKeeper上存在锁节点,那么进入等待
	if(zkClient.exists(PATH)) {
		//采用CountDownLatch等待
		latch = new CountDownLatch(1);
		try {
			//进入等待
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	//删除监听器
	zkClient.unsubscribeDataChanges(PATH, listen);
}

然后就是释放节点了:

@Override
public void unLock() {
	if(zkClient != null) {
		System.out.println(Thread.currentThread().getName() + " unlock.. ");
		//删除节点
		zkClient.delete(PATH);
		//关闭当前连接
		zkClient.close();
	}
}

这么一套流程下来,分布式锁的功能就完成了,当这种实现的功能类似JVM锁中的非公平锁,即没有先后顺序所言,如果想要达到公平锁,那么就必须得使用顺序节点进行操作了。

那么分布式公平锁监听的节点就不是同一个节点了,而是监听当前节点的上一个节点:

private String lockSeq = null;

private String before = null;

@Override
public boolean tryLock() {
    if(!zkClient.exists(PATH)) {
        try {
            zkClient.createPersistent(PATH, true);
        } catch (Throwable t) {
            //已经创建完毕,并发问题,抛异常处理
        }
    }
	if(lockSeq == null) {
		lockSeq = zkClient.createEphemeralSequential(PATH + "/", "1".getBytes());
	}
	List<String> children = zkClient.getChildren(PATH);
	if(lockSeq.equals(PATH + "/" + children.get(0))) {
		return true;
	}else {
		for(String str : children) {
			if(lockSeq.contains(str)) {
				break;
			}
			before = PATH + "/" + str;
		}
		System.out.println(Thread.currentThread().getName() + " before node:" + before);
		return false;
	}
}

@Override
public void waitLock() {

	IZkDataListener listen = new IZkDataListener() {
		public void handleDataChange(String dataPath, Object data) throws Exception {
		}

		public void handleDataDeleted(String dataPath) throws Exception {
			latch.countDown();
		}
		
	};

	zkClient.subscribeDataChanges(before, listen);
	if(zkClient.exists(before)) {
		latch = new CountDownLatch(1);
		try {
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	zkClient.unsubscribeDataChanges(before, listen);
	
}

@Override
public void unLock() {
	if(zkClient != null) {
		System.out.println(Thread.currentThread().getName() + " unlock.. ");
		System.out.println("this node:" + lockSeq + " last node:" + before);
		zkClient.delete(lockSeq);
	}
}

实现的代码如上(一个简单的实现方式,存在单应用并发问题,可以使用记录线程的方式解决并发问题),大家可以各位去试一下,个人觉得非公平锁的效率相比公平锁来说效率要高一点点,不过对应大量的分布式服务去竞争锁资源的话,个人建议还是使用公平锁,避免阻塞时间过长,导致服务业务长期停滞问题。

还会存在一个问题就是临时顺序节点在关闭服务的时候ZooKeeper上会等待几秒钟才会删除临时节点,所以建议在程序中加上Hook钩子方法进行删除。

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(ZkClientLock::run));
    }

    private static void run() {
        System.out.println("关闭服务...");
        //删除所有临时顺序节点,避免影响其他服务
        zkClient.delete(lockSeq);
        zkClient.close();
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
### 回答1: 使用 ZooKeeper 实现分布式锁最常见的方法是使用公平锁(Fair Lock)。在这种情况下,在 ZooKeeper 集群中创建一个特殊的临时节点,并使用它来保持锁定状态。当一个客户端尝试获取锁时,它会尝试创建这个临时节点,如果节点创建成功,则说明该客户端已经获得了锁。 使用 ZooKeeper 进行分布式锁实现需要使用 ZooKeeper 的原子操作,例如创建节点和监视节点,以保证锁的正确性。 代码示例如下: ```java public class DistributedLock { private ZooKeeper zk; private String lockNode; private String lockPath; public DistributedLock(ZooKeeper zk, String lockNode) { this.zk = zk; this.lockNode = lockNode; this.lockPath = "/locks/" + lockNode; } public void lock() throws Exception { while (true) { try { zk.create(lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); return; } catch (KeeperException.NodeExistsException e) { waitForLock(); } } } public void unlock() throws Exception { zk.delete(lockPath, -1); } private void waitForLock() throws Exception { CountDownLatch latch = new CountDownLatch(1); Watcher lockWatcher = new Watcher() { @Override public void process(WatchedEvent event) { if (event.getType() == EventType.NodeDeleted) { latch.countDown(); } } }; zk.exists(lockPath, lockWatcher); latch.await(); } } ``` 该代码示例提供了一个简单的实现,可以作为创建自己的分布式锁的基 ### 回答2: 在基于 ZooKeeper 实现 Java 分布式锁的过程中,可以按照以下步骤进行: 1. 创建一个基于 ZooKeeper 的客户端对象。 2. 在 ZooKeeper 上创建一个持久化的节点,作为锁的根节点。 3. 当需要进行锁操作时,创建一个临时有序节点作为当前请求的锁节点。 4. 调用 `getChildren` 方法获取锁根节点下的所有节点,并根据节点的序号进行排序。 5. 检查当前节点是否是锁根节点下序号最小的节点,如果是则获取到锁,执行业务逻辑。 6. 如果当前节点不是锁根节点下序号最小的节点,则注册监听锁根节点下序号比自己小一的节点。 7. 当监听到序号比自己小一的节点删除时,重复步骤 4-6 直到获取到锁。 8. 业务逻辑执行完成后,可以删除当前节点,释放锁资源。 此外,还需要特别注意以下几个问题: 1. 分布式锁的超时机制:在创建临时节点时,可以指定一个超时时间,当超过该时间后,如果还未获取到锁,可以删除当前节点,避免死锁。 2. 锁的释放:在业务逻辑执行完成后,需要手动删除当前节点。如果由于某些原因未能正常删除,则需要提供一种机制,在锁节点创建时设置一个 TTL(time-to-live),让 ZooKeeper 在锁节点过期后自动删除。 3. 锁节点的竞争:在并发较高的情况下,可能会出现多个客户端同时创建临时节点的情况。这时可以使用 `CyclicBarrier` 或者 `CountDownLatch` 进行同步,确保每次只有一个客户端创建锁节点。 4. 异常情况的处理:在进行锁操作时,需要处理各种异常情况,比如连接断开、网络超时等,保证系统的稳定性和可靠性。 综上所述,基于 ZooKeeper 可以实现 Java 分布式锁,通过创建临时有序节点和监听上一个节点的删除来实现锁的竞争和获取。 ### 回答3: 实现基于 ZooKeeper 的 Java 分布式锁可以遵循以下步骤: 1. 连接 ZooKeeper:首先,通过 Java API 连接到 ZooKeeper 服务器,可以使用 zookeeper API 提供的 ZooKeeper 类来创建一个连接对象。 2. 创建锁节点:在 ZooKeeper 上创建一个父节点作为锁的根节点,该节点的所有子节点都作为锁节点。可以使用 zookeeper API 的 create() 方法创建临时顺序节点。 3. 获取锁:每个需要获取锁的进程都要通过创建一个临时顺序节点来竞争锁。通过 zookeeper API 的 getChildren() 方法获取锁根节点的所有子节点,如果创建的节点序号是当前所有节点中最小的,则表示获取到了锁。 4. 监听锁节点变化:如果未能获取到锁,应该在创建节点后,使用 zookeeper API 的 exists() 方法注册一个监听器来监听创建的子节点。当监听到创建的子节点发生变化时,判断自己的节点是否变成了最小的节点,如果是则表示获取到了锁。 5. 释放锁:对于已经获取到锁的进程,执行完任务后,需要通过 zookeeper API 的 delete() 方法将自己创建的锁节点删除,这样其他进程就能获取到该锁了。 需要注意的是,在分布式环境下,网络通信可能会出现延迟或故障,因此需要考虑到这些情况来保证分布式锁的正确性和可靠性。此外,还需考虑到异常情况处理、死锁检测和容错等问题,以确保分布式锁的高可用性和可靠性。 以上是使用 ZooKeeper 实现 Java 分布式锁的基本步骤,通过合理地使用 ZooKeeper 的 API,可以轻松实现分布式环境下的锁机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值