一、zookeeper简介及基本操作
Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。当对目录节点监控状态打开时,一旦目录节点的状态发生变化,Watcher 对象的 process 方法就会被调用。
创建Zookeeper实例时即可绑定一个Watcher对象,如 ZooKeeper zk = new ZooKeeper(zookeeperQuorum, sessionTimeout, Watcher; 任何实现org.apache.zookeeper.Watcher接口的类都可作为一个Watcher对象。
zookeeperQuorum=IP+端口(xxx.xxx.xxx.xxx:2181,xxx.xxx.xxx.xxx:2181,xxx.xxx.xxx.xxx:2181)多个逗号隔开
可以设置观察的操作:exists,getChildren,getData
可以触发观察的操作:create,delete,setData
二、基于zookeeper的分布式锁原理
让我们来回顾一下Zookeeper节点的概念:
Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。
Znode分为四种类型:
1.持久节点 (PERSISTENT)
默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)
所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:
3.临时节点(EPHEMERAL)
和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:
4.临时顺序节点(EPHEMERAL_SEQUENTIAL)
顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
Zookeeper分布式锁的原理
Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:
获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的
释放锁
释放锁分为两种情况:
1.任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令。
2.任务执行过程中,客户端崩溃
获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。
最终,Client3成功得到了锁。
三、基于zookeeper的分布式锁代码实现
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 DistributeLock implements Watcher{
private ZooKeeper zk;
//当前锁
private String current_lock;
//竞争的资源
private String lockName;
//根节点
private String ROOT_LOCK = "/dlock";
//由于zookeeper监听节点状态会立即返回,所以需要使用CountDownLatch(也可使用信号量等其他机制)
private CountDownLatch latch;
public DistributeLock(String zkAddress, String lockName) {
this.lockName = lockName;
try {
zk = new ZooKeeper(zkAddress, 30000, this);
//获取根节点状态
Stat stat = zk.exists(ROOT_LOCK, false);
//如果根节点不存在,则创建根节点,根节点类型为永久节点
if(stat == null) {
System.out.println("根节点不存在");
zk.create(ROOT_LOCK, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//获取锁
public void lock() {
try {
//在根节点下创建临时顺序节点,返回值为创建的节点路径
current_lock = zk.create(ROOT_LOCK + "/" + lockName, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//获取根节点下的所有临时顺序节点,不设置监视器
List<String> children = zk.getChildren(ROOT_LOCK, false);
//对根节点下的所有临时顺序节点进行从小到大排序
Collections.sort(children);
//判断当前节点是否为最小节点,如果是则获取锁,若不是,则找到自己的前一个节点,监听其存在状态
int curIndex = Collections.binarySearch(children, current_lock.substring(current_lock.lastIndexOf("/") + 1));
// if(current_lock.equals(ROOT_LOCK + "/" + children.get(0))) {
if(curIndex == 0) {
System.out.println("获取锁成功");
return;
}else {
//获取当前节点前一个节点的路径
// String prev = children.get(Collections.binarySearch(children, current_lock) - 1);
String prev = children.get(curIndex - 1);
//监听当前节点的前一个节点的状态
Stat stat = zk.exists(ROOT_LOCK + "/" + prev, true);
//此处再次判断该节点是否存在,该步骤也可省略
if(stat == null) {
System.out.println("获取锁成功");
return;
}else {
System.out.println("等待锁......");
latch = new CountDownLatch(1);
//进入等待锁状态
latch.await();
System.out.println("获取锁成功");
latch = null;
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//释放锁
public void unlock() {
try {
//删除创建的节点
zk.delete(current_lock, -1);
current_lock = null;
//关闭zookeeper连接
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public void process(WatchedEvent event) {
if(this.latch != null) {
latch.countDown();
}
}
}
启动多个进程进行测试,将以下代码复制多份,启动多个进程,观察输出结果,可以看出已成功实现多进程分布式锁
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test1{
public static void main(String[] args) throws Exception {
DistributeLock lock = new DistributeLock("127.0.0.1:2181", "lock");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
lock.lock();
System.out.println(sdf.format(new Date()) + "开始执行业务......");
Thread.sleep(30000);
System.out.println(sdf.format(new Date()) + "业务处理结束......");
lock.unlock();
}
}