【中间件 | 分布式 | Java】zookeeper实现分布式锁原理及代码实现

本文介绍了Zookeeper作为分布式协调服务的基础,包括核心概念如Session和数据模型,特别是分布式锁的实现原理,如通过znode节点命名唯一和临时顺序节点结合watch机制。还提供了Java代码示例,并探讨了不同实现方式的适用场景,以及如何通过Docker安装Zookeeper。

zookeeper官网地址
http://zookeeper.apache.org/

zookeeper也被称为分布式环境指挥官、分布式系统协调服务,是分布式系统中很常见的一个基础组件。本文不对zookeeper做过多赘述,只简单介绍一下zk及其相关特性,以便于理解使用zk做分布式锁的便利性。

一、什么是zookeeper

Apache ZooKeeper是一种用于分布式系统的高性能协调服务,提供一种集中式信息存储服务。

特点:数据存储于内存中,类似于文件系统中的属性结构(文件和目录),高吞吐量和低延迟,集群可用性高

结构:zookeeper是C/S结构、即Client-Server在这里插入图片描述
应用:基于zookeeper可以实现分布式统一配置中心、服务注册中心、分布式锁、分布式队列、命名服务、Master选举…等等功能。

小结:
zk 实质上就是一个高可用、高性能的小量数据存取服务,结合它的特性(临时顺序节点、watch机制、全局有序事务...),特别适合做分布式系统协调服务。

二、zookeeper核心概念

1. Session - 会话

  • zk客户端与zk服务端连接之后,产生一个会话;
  • 一个客户端连接一个会话,由zk分配唯一会话ID;
  • 客户端以特定时间间隔发送心跳以保持会话有效;
  • 超过会话时间未收到客户端心跳,则判定客户端死了;
  • 会话中请求按FIFO(先进先出)顺序执行。
    在这里插入图片描述

2. 数据模型

以 / 为根节点
节点可以包含数据以及子节点(既是文件也是文件夹)
节点的路径总是表示为规范的、绝对的、斜杠(/)分隔的路径

znode
zookeeper中,每个节点也被被称为一个znode;特点:名称唯一,命名规范

2.1 znode - 节点类型

持久节点:不delete就一直在zk中存在

##PERSISTENT
[zk: localhost:2181(CONNECTED) 1] create /jin/y1 ""
Created /jin/y1

临时节点:客户端与zk服务端会话结束后就会被删除

##EPHEMERAL
[zk: localhost:2181(CONNECTED) 3] create -e /jin/y3 ""
Created /jin/y3

顺序节点:zookeeper给该节点名称进行顺序编号

  • 十位十进制序号
  • 每个父节点有一个计数器
  • 计数器是带符号int(4字节),到2147483647之后将溢出(导致名称“ -2147483648”)
##PERSISTENT_SEQUENTIAL
[zk: localhost:2181(CONNECTED) 2] create -s /jin/y2 ""
Created /jin/y20000000001

临时顺序节点:会话结束就被删除的顺序节点

##EPHEMERAL_SEQUENTIAL
[zk: localhost:2181(CONNECTED) 4] create -s -e /jin/y4 ""
Created /jin/y40000000001

2.2 watch监听机制

监听如:节点的变化、节点是否存在、子节点的变化...

客户端可以在znode上设置watch, 监听znode的变化;znode发生变化时通知客户端。

注意
​ 1)因为watch是一次性触发器,获取时间和设置下一个watch有延迟,所以对监听没有强可靠性
​ 2)一个watch对象只会被特定的通知触发一次

三、zookeeper实现分布式锁原理

方式一:znode节点命名唯一 + watch机制

原理:znode节点命名唯一 + watch机制

  1. 争抢创建同名的临时节点
  2. 创建成功即抢到锁,执行业务完业务代码,删掉节点
  3. 创建不成功就创建watcher监控锁,阻塞等待
  4. watch监测到锁节点被删除,取消watcher继续争抢锁,重复上述步骤

为何是用临时节点?
防止获得锁的节点出现问题,down掉之后未释放锁

缺点:惊群效应 (并发量大的时候,反复唤醒大量线程,占用系统资源,会造成很大的网络冲击,甚至可能会让zk服务挂掉)
总结:实现简单,适用于并发量小的情况。

方式二:临时顺序节点 + watch机制

原理:临时顺序节点 + watch机制

  1. 需要争抢锁的线程,在同一路径下依次创建顺序节点
  2. 创建完成后,获得所有子节点,判断自己是否是当前最小号
  3. 是最小号获得锁
  4. 不是最小号,对前一个节点注册watcher,阻塞等待
  5. 获得锁的节点释放锁后重复上述步骤

总结:避免了方式一的惊群效应问题,适合高并发场景。

四、Java代码实现

这里对方式二的分布式锁,做一个简单的实现;

使用的API是zkClient

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

相关变量定义:

public class ZKDistributedLock implements Lock {
	// 锁路径(临时顺序节点们的父路径)
	private String lockPath;
	// zk客户端
	private ZkClient client;
	// 这里为了方便,用ThreadLocal实现的线程安全,也可以用其他方式
	// 当前节点的路径
	private ThreadLocal<String> currentPath = new ThreadLocal<>();
	// 前一个节点的路径
	private ThreadLocal<String> beforePath = new ThreadLocal<>();
	// 锁重入计数
	private ThreadLocal<Integer> reentrantCount = new ThreadLocal<>();

   // 下面是复写Lock的方法,这里无需关注
	@Override
	public void lockInterruptibly() throws InterruptedException {
		// TODO Auto-generated method stub
	}
	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// TODO Auto-generated method stub
		return false;
	}
	@Override
	public Condition newCondition() {
		// TODO Auto-generated method stub
		return null;
	}
}

构造方法:

	public ZKDistributedLock(String lockPath) {
		super();
		this.lockPath = lockPath;
		client = new ZkClient("host:2181");
		client.setZkSerializer(new MyZkSerializer());
		if (!this.client.exists(lockPath)) {
			try {
				this.client.createPersistent(lockPath);
			} catch (ZkNodeExistsException e) {
				// 无需处理,已存在就直接用即可
			}
		}
	}

tryLock()逻辑:

	@Override
	public boolean tryLock() {
		if (this.reentrantCount.get() != null) {
			int count = this.reentrantCount.get();
			if (count > 0) {
				this.reentrantCount.set(++count);
				return true;
			}
		}

		if (this.currentPath.get() == null) {
		// 创建临时顺序节点,zk中创建节点必须赋值,所以这里随便赋值aaa
		currentPath.set(this.client.createEphemeralSequential(lockPath + "/", "aaa"));
		}
		// 获得所有的子
		List<String> children = this.client.getChildren(lockPath);

		// 排序list
		Collections.sort(children);

		// 判断当前节点是否是最小的
		if (currentPath.get().equals(lockPath + "/" + children.get(0))) {
			this.reentrantCount.set(1);
			return true;
		} else {
			// 取到前一个
			// 得到字节的索引号
			int curIndex = children.indexOf(currentPath.get().substring(lockPath.length() + 1));
			beforePath.set(lockPath + "/" + children.get(curIndex - 1));
		}
		return false;
	}

lock()逻辑:

@Override
	public void lock() {
		if (!tryLock()) {
			// 阻塞等待
			waitForLock();
			// 再次尝试加锁
			lock();
		}
	}

	private void waitForLock() {
		CountDownLatch cdl = new CountDownLatch(1);

		// 注册watcher
		IZkDataListener listener = new IZkDataListener() {

			@Override
			public void handleDataDeleted(String dataPath) throws Exception {
				System.out.println("-----监听到节点被删除----");
				cdl.countDown();
			}

			@Override
			public void handleDataChange(String dataPath, Object data) throws Exception {

			}
		};
		client.subscribeDataChanges(this.beforePath.get(), listener);

		
		if (this.client.exists(this.beforePath.get())) {
			try {
				// 阻塞
				cdl.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// 醒来后,取消watcher
		client.unsubscribeDataChanges(this.beforePath.get(), listener);
	}

unlock()逻辑

@Override
	public void unlock() {
		// 重入的释放锁处理
		if (this.reentrantCount.get() != null) {
			int count = this.reentrantCount.get();
			if (count > 1) {
				this.reentrantCount.set(--count);
				return;
			} else {
				this.reentrantCount.set(null);
			}
		}
		// 删除当前节点
		this.client.delete(this.currentPath.get());
	}

测试:

	public static void main(String[] args) {
		// 并发数
		int currency = 50;

		// 利用CyclicBarrier模拟并发场景,CyclicBarrier的作用是集满指定线程个数,一起发出去
		CyclicBarrier cb = new CyclicBarrier(currency);
		
		for (int i = 0; i < currency; i++) {
			new Thread(new Runnable() {
				public void run() {					System.out.println(Thread.currentThread().getName() + "---------Ready---------------");
					// 等待一起出发
					try {
						cb.await();
					} catch (InterruptedException | BrokenBarrierException e) {
						e.printStackTrace();
					}
					ZKDistributedLock lock = new ZKDistributedLock("/distLock");

					try {
						lock.lock();
						System.out.println(Thread.currentThread().getName() + " 获得锁!");
					} finally {
						lock.unlock();
					}
				}
			}).start();

		}
	}

如果使用zookeeper原生方式来用代码实现分布式锁的话还是比较繁琐的,日常开发中可以使用Apache的开源客户端Curator封装好的分布式锁。
推荐一篇看到的好文:
Curator实现分布式锁的基本原理

五、docker安装zk

随便找了一个,可以在自己虚拟机上试试命令行

docker pull zookeeper
docker run --privileged=true -d --name zookeeper --publish 2181:2181  -d zookeeper:latest

有需要也可以自行安装window、mac版本的zookeeper

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值