ZooKeeper总结学习 第五课 -基于zk的分布式锁
一、分布式锁介绍
分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性。
首先我们先来看看使用zk实现分布式锁的原理,在zk中是使用文件目录的格式存放节点内容,其中节点类型分为:
- 持久节点(PERSISTENT ):节点创建后,一直存在,直到主动删除了该节点。
- 临时节点(EPHEMERAL):生命周期和客户端会话绑定,一旦客户端会话失效,这个节点就会自动删除。
- 序列节点(SEQUENTIAL ):多个线程创建同一个顺序节点时候,每个线程会得到一个带有编号的节点,节点编号是递增不重复的。
二、分布式锁获取思路
1.获取分布式锁的总体思路
a、在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。
b、客户端调用createNode方法在locker下创建临时顺序节点,然后调用getChildren(“locker”)来获取locker下面的所有子节点,注意此时不用设置任何Watcher。
c、客户端获取到所有的子节点path之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。
d、如果发现自己创建的节点并非locker所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。
e、之后,让这个被关注的节点删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
2.获取分布式锁的核心算法流程
下面同个一个流程图来分析获取分布式锁的完整算法,如下:
解释:
客户端A要获取分布式锁的时候首先到locker下创建一个临时顺序节点(node_n),然后立即获取locker下的所有(一级)子节点。
此时因为会有多个客户端同一时间争取锁,因此locker下的子节点数量就会大于1。对于顺序节点,特点是节点名称后面自动有一个数字编号,
先创建的节点数字编号小于后创建的,因此可以将子节点按照节点名称后缀的数字顺序从小到大排序,这样排在第一位的就是最先创建的顺序节点,
此时它就代表了最先争取到锁的客户端!
此时判断最小的这个节点是否为客户端A之前创建出来的node_n,如果是则表示客户端A获取到了锁,
如果不是则表示锁已经被其它客户端获取,因此客户端A要等待它释放锁,也就是等待获取到锁的那个客户端B把自己创建的那个节点删除。
此时就通过监听比node_n次小的那个顺序节点的删除事件来知道客户端B是否已经释放了锁,如果是,此时客户端A再次获取locker下的所有子节点,
再次与自己创建的node_n节点对比,直到自己创建的node_n是locker的所有子节点中顺序号最小的,此时表示客户端A获取到了锁!
下面是代码
1、 创建 zk连接客户端:
package com.dukun.study.config;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* 创建zk连接客户端
* Created by dukun on 2019/4/2.
*/
public class ZookeeperClient {
private final static String CONNECTIONSTRING="127.0.0.1:2181";
private static int sessionTimeout = 5000;
//获取连接
public static ZooKeeper getInstance() throws IOException, InterruptedException {
//利用这个wait方法保存已连接状态,有延时
final CountDownLatch countDownLatch = new CountDownLatch(1);
ZooKeeper zooKeeper = new ZooKeeper(CONNECTIONSTRING, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//判断是否已经连接上
if (watchedEvent.getState() == Event.KeeperState.SyncConnected){
countDownLatch.countDown();
}
}
});
countDownLatch.await();
return zooKeeper;
}
public static int getSessionTimeout() {
return sessionTimeout;
}
}
2: 创建监听方法:
package com.dukun.study.config;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import java.util.concurrent.CountDownLatch;
/**
* 监听节点被删除事件
* Created by dukun on 2019/4/2.
*/
public class LockWatcher implements Watcher{
private CountDownLatch latch;
public LockWatcher(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void process(WatchedEvent event) {
//判断是不是节点删除了
if(event.getType()== Event.EventType.NodeDeleted){
latch.countDown();
}
}
}
3 锁的实现:
package com.dukun.study.config;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.util.List;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁实现
* Created by dukun on 2019/4/2.
*/
public class DistributeLock {
private static final String ROOT_LOCK="/locks";//根节点
private ZooKeeper zooKeeper;
private int sessionTimeout;//会话超时时间
private String lockID;//记录锁节点ID
private final static byte[] data = {1,2};//节点数据
private CountDownLatch countDownLatch = new CountDownLatch(1);
public DistributeLock() throws IOException, InterruptedException {
this.zooKeeper = ZookeeperClient.getInstance();
this.sessionTimeout = ZookeeperClient.getSessionTimeout();
}
//获取锁的方法
public boolean lock(){
try {
//四个参数:路径、保存内容、权限、临时有序节点 LOCKS/0000000001
lockID = zooKeeper.create(ROOT_LOCK+"/",data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName()+"-->成功创建了lock节点,节点ID="+lockID+"开始去竞争锁");
//获取当前根节点下所有的节点,然后判断是不是最小节点
List<String> childrenNodes = zooKeeper.getChildren(ROOT_LOCK,true);
//排序从小到大
SortedSet<String> sortedSet = new TreeSet<String>();
for (String children : childrenNodes){
sortedSet.add(ROOT_LOCK+"/"+children);
}
String first = sortedSet.first();//拿到最小的节点
if (lockID.equals(first)){
//表示当前就是最小的节点
System.out.println(Thread.currentThread().getName()+"---->成功的获取锁.lock节点为="+lockID);
return true;
}
//拿到这个节点之前的所有节点,再拿最后一个节点,就是拿当前节点的上一个节点,用于监听变化
SortedSet<String> lessThanLockID = sortedSet.headSet(lockID);
if (!lessThanLockID.isEmpty()){
String prevLockID = lessThanLockID.last();
zooKeeper.exists(prevLockID,new LockWatcher(countDownLatch));
countDownLatch.await(sessionTimeout, TimeUnit.MILLISECONDS);
//上面这段代码意味着会话超时或者节点被删除(释放)了
System.out.println(Thread.currentThread().getName()+"成功获取锁,lockID="+lockID);
}
return true;
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
//释放锁的方法
public boolean unlock(){
try {
System.out.println(Thread.currentThread().getName()+"--->开始释放锁lock="+lockID);
zooKeeper.delete(lockID,-1);
System.out.println("节点"+lockID+"成功被删除");
return true;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
return false;
}
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(10);
Random random = new Random();
for (int i=0 ;i < 10;i++){
new Thread(()->{
DistributeLock lock = null;
try {
lock = new DistributeLock();
latch.countDown();
latch.await();
lock.lock();
Thread.sleep(random.nextInt(500));
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if (lock != null){
lock.unlock();
}
}
}).start();
}
}
}