基于zookeeper实现"有序"的分布式锁

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/s3395719/article/details/87901326

应用场景
说起分布式锁,第一时间想到的就是使用Redis来实现简单便捷,但是redis实现的分布式锁是无序的、不公平的。当有业务需求要求保证用户的访问顺序时,redis分布式锁是无法满足业务需求的。(ps:可能是我能力有限)
想到zookeeper也能实现分布式锁,上网一搜,果然zk不管有序无序都OJBK。

zookeeper特点
树形结构:zk类似于文件系统,从根节点出发,不断地扩展延伸。它的节点相当于文件夹,并且可以保存数据。
长连接:zk与client建立tcp长连接。client可以监听节点的状态,当节点有变化时,sever会主动通知client响应事件。
一致性:基于ZAB算法保证了zk数据一致性。

具体用途
配置中心,命名服务,分布式锁(队列),发布订阅,心跳检测…只要需要“中心化”的项目都可以使用。
如hadoop/hbase/kafka等等都使用到了zookeeper。

屁话不多说,下面进入正题
基本思路: 当client-1 在zk里面创建一个节点后,其它client是无法再创建同样名称的节点的,只能根据访问zk的先后顺序创建 锁名称+1的节点。如 第一个进入的叫 lock_001, 第二个则是lock_002,第三个是lock_003 ,第四个…。第一个创建节点的client相当于获得了锁,而其他clients,创建了lock_003节点的client会监听lock_002节点,创建了lock_002节点的client会监听lock_001节点,当前一个节点释放锁后,就代表轮到自己获得锁。(看不懂别看了,这里表达得不是很好)

举个生活中的例子,一群人到银行办理业务拿号(创建节点),你抢到了0010 的号码(创建了lock_010),坐一边等待(阻塞)。你不需要关注001~008的号动态,只需要关心009号的动态(监听lock_009),当009离开柜台时(释放锁),你就获得了柜台的“使用权”(获得锁)。

我在网上贴的代码基本上是会尽量避免使用方法封装代码,因为大家上网看文章目的性都是比较强的。有些人封装一大堆方法,实际开发是没问题,但其他人在网页上看得头都晕

package zookeeper.zookeeper;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
 

public class DistrubateLock implements Watcher,Runnable{

	private final int sessionTimeout = 300 * 1000;
	//zk连接
	public ZooKeeper zk = null;
	//zookeeper的远程ip地址
	private String address;
	//根节点名称
	private final String ROOT = "/lock";
	//锁的名称
	private String lockName;
	//每个DistrubateLock都有它名称,用于加锁/解锁
	private String myNode = null;
	//计数器
    private CountDownLatch countDownLatch;
	
    
	DistrubateLock(String address,String lockName){
		this.address = address;
		this.lockName = lockName;
	}
	
	/**
	 * 初始化zookeeper的配置参数,并且创建根节点
	 */
	public void init() {
		try {
			//创建连接,会有点慢
			zk = new ZooKeeper(address, sessionTimeout, this);
			//判断跟节点是否存在
            Stat stat = zk.exists(ROOT, false);
            if (stat == null) {
                // 如果根节点不存在,则创建根节点
                zk.create(ROOT, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
		} catch (IOException | KeeperException | InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	} 
	
	/**
	 * 创建锁
	 * @return
	 */
	public void getLock() {
		try {
			//锁的临时节点名称(完整路径/绝对地址)
			String tmpName = ROOT + "/" + lockName;
			//尝试创建锁的临时节点,返回该节点的名称,可以理解为获得了“锁的钥匙”
			this.myNode = zk.create(tmpName, new byte[0],
			        ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
			
			System.out.println(Thread.currentThread().getName() + ": 获得了钥匙" + this.myNode);//获得了锁
			//获取根节点下所有的临时节点
			List<String> nodes = zk.getChildren(ROOT, false);
			//节点名称排序,从小到大
			Collections.sort(nodes);
			
			String path = ROOT + "/";
			
			if(nodes.size() == 0) {
				throw new InterruptedException();
			}
			//拿出队列最小的一把锁(节点),和自己所拥有的“钥匙”对比,匹配就获得锁,不匹配则等待
			if(myNode.contains(nodes.get(0))){
				//获得了锁,其实10个线程中,只有最快(No.1)的一个线程能进入这个条件
				System.out.println(Thread.currentThread().getName() + ": 获得了锁..., 钥匙是:" + this.myNode);
			}else { 
				//查询刚刚得到的列表,获得你前一把锁的名称
				String preNode = path + nodes.get(Collections.binarySearch(nodes, myNode.replace(path, "")) - 1);
				//监听它解锁事件
				Stat stat = zk.exists(preNode, true);
				if(stat != null) {
					//计数器,countDownLatch.await()会阻塞程序,当countDownLatch = 0 时被唤醒,不熟悉的可以百度一下
					countDownLatch = new CountDownLatch(1);
					countDownLatch.await();
					//被唤醒了,获得了锁,ojbk执行业务去了
					System.out.println(Thread.currentThread().getName() + ": 获得了锁..., 钥匙是:" + this.myNode);//获得了锁
				}
			}
		} catch (KeeperException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
	}
	
	/**
	 * 解锁(删除节点)
	 */
	public void removelock() {
		try {
			System.out.println(Thread.currentThread().getName() + ": 解锁了...\n");
			zk.delete(myNode, -1);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (KeeperException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * process 是 Watcher接口的实现方法,当节点被删除了,就会触发。ps:监听一次,只触发一次
	 */
	@Override
	public void process(WatchedEvent event) {
		if(countDownLatch != null) {
			//计数器减1,countDownLatch = 0 就会唤醒那个阻塞的业务线程了
			countDownLatch.countDown();
		}
	} 

	@Override
	public void run() {
		//初始化zookeeper的连接配置
		this.init();
		//获得锁
		this.getLock();
		try {
			//假装执行了一秒的业务逻辑
			System.out.println(Thread.currentThread().getName() + ": 假装执行了一秒的业务逻辑");
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//解锁
		this.removelock();
	}
	
	public static void main(String[] args) throws KeeperException, InterruptedException {
		for(int i = 0 ; i < 10 ; i++) {
			//创建10个新对象,模拟10个Client同时竞争锁
			DistrubateLock lock = new DistrubateLock("zookeeper IP:2181","tmp_lock");
			new Thread(lock).start();
		}
	}
}

补充一点:无序的分布式锁,原理大致差不多,只需去掉代码的排序部分,以及每次触发事件(process)后需要重新去监听节点。

猜你喜欢

转载自blog.csdn.net/s3395719/article/details/87901326