基于ZooKeeper原生API实现分布式锁
原创
©著作权归作者所有:来自51CTO博客作者Dongguabai的原创作品,请联系作者获取转载授权,否则将追究法律责任
其实实现分布式锁主要需要有一个第三方中间件能够提供锁的存储和锁的释放。像数据库、Redis、ZooKeeper都是常用的分布式锁解决方案。
分析
根据ZK节点的特性,在同一时间内,只会有一个客户端创建/Locks/lock节点成功,失败的节点则会监听/Locks/lock节点的变化:
当/Locks/lock发生变化后,会触发事件的监听机制,随后ClientB和ClientA客户端会重新再去争抢这把锁。这是一个比较简单的实现分布式锁的思想,但是这个会产生“惊群效应”,在并发量较高的情况下,也就是说短时间之内会有大量的客户端去争抢这把锁,短时间内会发生大量的事件上下文变更,但是实际上只有一个客户端可以抢到锁,相当于出现了大量的无效的系统调度、上下文切换,系统系能大打折扣。
为了解决这个问题,我们可以利用ZooKeeper中有序节点的特性。每个客户端都去/Locks节点下创建一个有序节点/Locks/lock_seq_000X,这样每一个客户端都与一个有序节点有了关联关系。如果要获得锁的话,只需要从这些所有的有序节点中获得一个最小的子节点,也就是说这个节点对应的客户端可以获得锁。其他的次于最小节点的节点只监听比自己小1的节点:
当一个节点发生变化,监听它的节点能够收到这样一个变化。再去判断当前节点是不是所有节点中最小的节点,如果是的就获得锁,不是的就继续等待,直到上一个节点释放。这样就避免了惊群效应。
代码示例
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 扩展Lock接口实现分布式锁
*
* @author Dongguabai
* @date 2018/10/30 11:36
*/
@Slf4j
public class DistributedLock implements Lock, Watcher {
private ZooKeeper zk = null;
/**
* 定义根节点
*/
private String ROOT_LOCK = "/locks";
/**
* 表示等待前一个锁
*/
private String WAIT_LOCK;
/**
* 表示当前锁
*/
private String CURRENT_LOCK;
/**
* 主要用作控制
*/
private CountDownLatch countDownLatch;
public DistributedLock() {
try {
zk = new ZooKeeper("192.168.220.136,192.168.220.137", 4000, this);
//为false就不去注册当前的事件
Stat stat = zk.exists(ROOT_LOCK, false);
//判断当前根节点是否存在
if (stat == null) {
//创建持久化节点
zk.create(ROOT_LOCK, "0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
log.error("初始化分布式锁异常!!", e);
}
}
@Override
public void lock() {
if (tryLock()){
//如果获得锁成功
log.info(Thread.currentThread().getName()+"-->"+CURRENT_LOCK+"|获得锁成功!恭喜!");
return;
}
//如果没有获得锁,那么就继续监听,等待获得锁
try {
waitForLock(WAIT_LOCK);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 持续阻塞获得锁的过程
* @param prev 当前节点的前一个等待节点
* @return
*/
private boolean waitForLock(String prev) throws KeeperException, InterruptedException {
//等待锁需要监听上一个节点,设置Watcher为true,即每一个有序节点都去监听它的上一个节点
Stat stat = zk.exists(prev,true);
if (stat!=null){
//即如果上一个节点依然存在的话
log.info(Thread.currentThread().getName()+"-->等待锁 "+ROOT_LOCK+"/"+prev+"释放。");
countDownLatch = new CountDownLatch(1);
countDownLatch.await();
log.info(Thread.currentThread().getName()+"-->"+"等待后获得锁成功!");
}
return true;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
//创建临时有序节点(节点会自动递增)-当前锁
CURRENT_LOCK = zk.create(ROOT_LOCK + "/", "0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
log.info(Thread.currentThread().getName()+"-->"+CURRENT_LOCK+"|尝试竞争锁!");
//获取根节点下所有的子节点,不注册监听
List<String> children = zk.getChildren(ROOT_LOCK, false);
//排序
SortedSet<String> sortedSet = new TreeSet<>();
children.forEach(child->{
sortedSet.add(ROOT_LOCK+"/"+child);
});
//获取当前子节点中最小的节点
String firstNode = sortedSet.first();
if (StringUtils.equals(firstNode,CURRENT_LOCK)){
//将当前节点和最小节点进行比较,如果相等,则获得锁成功
return true;
}
//获取当前节点中所有比自己更小的节点
SortedSet<String> lessThenMe = sortedSet.headSet(CURRENT_LOCK);
//如果当前所有节点中有比自己更小的节点
if (CollectionUtils.isNotEmpty(lessThenMe)){
//获取比自己小的节点中的最后一个节点,设置为等待锁
WAIT_LOCK = lessThenMe.last();
}
return false;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
log.info(Thread.currentThread().getName()+"-->释放锁 "+CURRENT_LOCK);
try {
//-1强制删除
zk.delete(CURRENT_LOCK,-1);
CURRENT_LOCK = null;
zk.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@NotNull
@Override
public Condition newCondition() {
return null;
}
/**
* 监听事件
* @param event
*/
@Override
public void process(WatchedEvent event) {
if (this.countDownLatch!=null){
//如果不为null说明存在这样的监听
this.countDownLatch.countDown();
}
}
}
测试代码:
import java.util.concurrent.CountDownLatch;
/**
* @author Dongguabai
* @date 2018/10/30 12:40
*/
public class Test {
static Integer ALL = 10;
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 10; j++) {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// DistributedLock lock= new DistributedLock();
// lock.lock();
System.out.println(Thread.currentThread().getName() + "卖出第" + ALL-- + "张票!");
// lock.unlock();
}, "售票员<" + i + ">").start();
countDownLatch.countDown();
}
Thread.sleep(1500);
ALL=10;
System.out.println("========================");
}
}
}
分析代码:
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* @author Dongguabai
* @date 2018/10/30 12:40
*/
public class Test {
public static void main(String[] args) throws IOException {
for (int j = 0; j < 10; j++) {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
try {
countDownLatch.await();
DistributedLock lock = new DistributedLock();
lock.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread<" + i + ">").start();
countDownLatch.countDown();
}
System.in.read();
}
}
}
输出内容:
可以看到Thread<10>获得了70这把锁,而Thread<4>等待70这把锁释放,也就是说当70释放后肯定是Thread<4>获得锁,进入ZK客户端:
直接delete70节点,即在等待70锁释放的线程会获取到锁:
这时候控制台输出:
结合之前的逻辑,会监控比自己小1的节点,现在Thread<4>获得了锁,结合之前控制台的输出内容,71是Thread<3>在监控,现在删除71节点,那么Thread<3>会获得锁:
控制台输出:
这里只是简单的基于原生API实现了分布式锁,代码是比较复杂的,待优化地方很多而且在tryLock()中没有实现时间控制,而且锁其实也有很多种。如果要使用ZK实现分布式锁,最好的方式还是基于Curator来做。不过Curator实现分布式锁的原理也和这里介绍的差不多。