概念与分析
什么是分布式锁
传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制,但是原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效。提出分布式锁的概念,是为了解决跨机器的互斥机制来控制共享资源的访问。
当一个进程使用一个资源时,会去请求对这个资源的锁,以求对这个资源的独占,使得其他进程无法访问该资源。当进程使用完该资源,会释放锁,其他进程获得锁,再去使用该资源。我们将为保证分布式系统中多个进程有序访问临界资源的锁机制称为分布式锁。
zookeeper分布式锁分析
zookeeper分布式锁在客户端(对zookeeper集群而言)向zookeeper集群进行了上线注册并在一个永久节点下创建有序的临时子节点后,根据编号顺序,最小顺序的子节点获取到锁,其他子节点由小到大监听前一个节点。
当拿到锁的节点处理完事务后,释放锁,后一个节点监听到前一个节点释放锁后,立刻申请获得锁,以此类推
过程解析
第一部分:客户端在zookeeper集群创建带序号的、临时的节点
第二部分:判断节点是否是最小的节点,如果是,获取到锁,如果不是,监听前一个节点
分布式锁实现
在idea新建maven项目,名为com.heria.distributedlock
在项目下新建javaclass,名为Distributedlock
获取与zookeeper的连接
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
}
});
如果根节点路径不存在,就创建路径,如果路径存在,就不用执行创建根节点的步骤
Stat stat = zk.exists("/locks", false);
if(stat== null){
//创建一下根节点
zk.create("/locks","locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
判断创建的节点是否是最小的序号节点,如果是,获取到锁,如果不是,监听前一个序号的节点
if(children.size()==1){
return;
}else {
Collections.sort(children);
//获取节点名称
String thisNode = currentMode.substring("/locks/".length());
//通过名称获取该结点在children的位置
int index = children.indexOf(thisNode);
//判断
if(index==-1){
System.out.println("数据异常");
}else if (index==0){
//就一个节点,可以获取锁
return;
}
else{
//需要监听前一个节点变化
waitPath="/locks/"+children.get(index-1);
zk.getData("waitPath",true,null);
//等待监听
waitLatch.await();
return;
}
同时,需要在监听器中设置机制
public void process(WatchedEvent watchedEvent) {
//connectLatch 如果连接上zk,可以释放
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
connectLatch.countDown();
}
//waitLatch参数控制//等待前一个步骤执行完后在开始
//waitLatch需要释放(节点被删除并且被删除的节点的路径是前一个节点的路径)
if(watchedEvent.getType()== Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath));
}
对zk进行解锁
public void unZklock() throws InterruptedException, KeeperException {
//删除节点
zk.delete(currentMode, -1);
}
完整代码
package com.heria.distributedlocks;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class DistributedLock {
private final String connectString="master:2181,slave1:2181,slave2:2181";
private final int sessionTimeout=2000;
private final ZooKeeper zk;
private CountDownLatch connectLatch=new CountDownLatch(1);
//等待前一个步骤执行完后在开始
private CountDownLatch waitLatch=new CountDownLatch(1);
private String waitPath;
private String currentMode;
public DistributedLock() throws IOException, InterruptedException, KeeperException {
//获取链接
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//connectLatch 如果连接上zk,可以释放
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
connectLatch.countDown();
}
//waitLatch需要释放(节点被删除并且被删除的是前一个节点)
if(watchedEvent.getType()== Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){
waitLatch.countDown();
}
}
});
//等待zk正常连接,往下走程序
connectLatch.await();
//判断节点/locks是否存在
Stat stat = zk.exists("/locks", false);
if(stat== null){
//创建一下根节点
zk.create("/locks","locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
//对zk加锁
public void zklock(){
//创建对应的临时带序号的节点
try {
currentMode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//判断创建的节点是否是最小的序号节点
List<String> children = zk.getChildren("/locks", false);
//如果创建的节点只有一个值,就直接获取到锁,如果不是,监听他前一个节点
if(children.size()==1){
return;
}else {
Collections.sort(children);
//获取节点名称
String thisNode = currentMode.substring("/locks/".length());
//通过名称获取该结点在children的位置
int index = children.indexOf(thisNode);
//判断
if(index==-1){
System.out.println("数据异常");
}else if (index==0){
//就一个节点,可以获取锁
return;
}
else{
//需要监听前一个节点变化
waitPath="/locks/"+children.get(index-1);
zk.getData(waitPath,true,null);
//等待监听
waitLatch.await();
return;
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//对zk加锁
public void unZklock() throws InterruptedException, KeeperException {
//删除节点
zk.delete(currentMode, -1);
}
}
测试
在com.heria.distributedlock中新建java class,命名为DistributedLockTest
完整代码
package com.heria.distributedlocks;
import org.apache.zookeeper.KeeperException;
import java.io.IOException;
public class DistributedLockTest {
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
final DistributedLock lock1 = new DistributedLock();
final DistributedLock lock2 = new DistributedLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock1.zklock();
System.out.println("线程1启动,获取到锁");
Thread.sleep(5*1000);
lock1.unZklock();
System.out.println("线程1释放锁");
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock2.zklock();
System.out.println("线程2启动,获取到锁");
Thread.sleep(5 * 1000);
lock2.unZklock();
System.out.println("线程2释放锁");
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}).start();
}
}
解析
1.创建两个客户端
final修饰过后的对象不能改变其的引用,但能修改属性值
final DistributedLock lock1 = new DistributedLock();
final DistributedLock lock2 = new DistributedLock();
2.两个客户端以同样的代码创建-休眠-释放锁,由于start()函数是异步的,因此谁先被cpu调度,谁就获取到锁,在前一个客户端释放锁之后,第二个客户端再获取锁
new Thread(new Runnable() {
@Override
public void run() {
try {
lock1.zklock();
System.out.println("线程1启动,获取到锁");
Thread.sleep(5*1000);
lock1.unZklock();
System.out.println("线程1释放锁");
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock2.zklock();
System.out.println("线程2启动,获取到锁");
Thread.sleep(5*1000);
lock2.unZklock();
System.out.println("线程2释放锁");
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}).start();
运行结果
能够看到两个线程先获取到锁的顺序是随机的,在一个线程获取到锁之后,另一个线程等待,直到获取到锁的线程释放锁。