一、分布式锁的初识
1.分布式锁简介
我们都知道,在JDK中,我们可以通过synchronized关键字和Lock实现同步锁,也称本地锁。一般我们用其在多线程环境中控制对资源的并发访问。本地锁有其局限性,本地锁仅适用于单个JVM进程。试想,随着业务的快速发展,单机应用势必会被替代,取而代之的将是分布式集群部署。在分布式环境中,本地锁将失去其应有效用。由此,分布式锁应运而生!
2.分布式锁特性
- 互斥性:和我们本地锁一样互斥性是最基本的。但是分布式锁需要保证在不同节点的不同线程的互斥。
- 可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
- 锁超时:和本地锁一样支持锁超时,防止死锁。
- 高可用:加锁和解锁需要高效,同时也需要保证高可用。防止分布式锁失效,可以增加降级。
- 支持阻塞和非阻塞:和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
- 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。
3.分布式锁常见解决方案
分布式锁较为常见的解决方案有:
- 3.1 借助 Redis 的 key 过期策略
- 3.2 借助 Zookeeper 中持久节点和临时节点
本文选用的是3.2的解决方案
二、使用Zookeeper实现分布式锁
zookeeper实现分布式锁的思路梳理:
- 2.1 创建一个持久性节点,作为分布式锁节点的工作空间。
- 2.2 在2.1所在节点,创建临时节点(特点:会话断开,节点自动移除)tn。若tn创建成功,表明分布式锁抢占成功;若tn创建异常,表明该轮抢占分布式锁未能成功,则进入等待状态,等待分布式锁的释放。
- 2.3 在2.2创建临时节点成功的线程,将获得分布式锁,可以继续执行相关业务逻辑,执行完成后,应保证及时将分布式锁释放(即移除2.1创建的临时节点),并通知唤醒其他等待态的线程,进而参与新一轮的分布式锁竞争。
- 2.4 在2.2创建临时节点发生异常,线程同样应该进入等待状态,等待被唤醒,方能参与新一轮的分布式锁竞争。
代码实现
- 引入Jar依赖:curator-recipes 是 zk 常用的Java客户端之一
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
- 使用Zookeeper实现分布式锁
import com.itbounds.demo.locks.config.MyConfig;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs.Ids;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* @Description 分布式锁的Zookeeper实现
* @Author blake
* @Date 2020/4/12 10:37 下午
* @Version 1.0
*/
public class ZkDistributedLock {
// zkClient instance
private CuratorFramework zkClient;
// WorkSpace
private static final String WORKSPACE = "/lock_workspace";
// 锁名称 - 对应业务类型
private String lockName;
public ZkDistributedLock(String lockName) {
this.lockName = lockName;
// 初始化
init();
}
public void init() {
// 获取zk客户端
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(
MyConfig.class);
zkClient = (CuratorFramework)applicationContext.getBean(CuratorFramework.class);
zkClient.start();
// 创建workspace - 使用持久节点
try {
if (Objects.isNull(zkClient.checkExists().forPath(WORKSPACE))) {
zkClient.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.withACL(Ids.OPEN_ACL_UNSAFE)
.forPath(WORKSPACE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 使用 zk 的临时节点
public Boolean getLock() {
while (true) {
String lockPath = WORKSPACE + "/" + lockName;
try {
if (Objects.isNull(zkClient.checkExists().forPath(lockPath))) {
zkClient.create().creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.withACL(Ids.OPEN_ACL_UNSAFE)
.forPath(lockPath);
System.out.println(" get lock successfully! ");
return true;
} else {
// 注册监听 & 进入阻塞
registerWatcherAndAwait();
System.out.println(" get lock failure! ");
return false;
}
} catch (Exception e) {
// 注册监听 & 进入阻塞
try {
registerWatcherAndAwait();
System.out.println(" get lock failure! ");
return false;
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
/**
* 添加监听的同时进入阻塞 & 临时节点删除事件触发再将线程唤醒
*/
public void registerWatcherAndAwait() throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
PathChildrenCache childrenCache = new PathChildrenCache(zkClient, WORKSPACE, true);
childrenCache.start();
childrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client,
PathChildrenCacheEvent event) throws Exception {
if (event.getType().equals(Type.CHILD_REMOVED) && event.getData().getPath()
.contains(lockName)) {
// 唤醒当前线程
countDownLatch.countDown();
}
}
});
// 线程挂起
countDownLatch.await();
}
/**
* 释放锁:即 移除分布式锁标识的zk节点
*/
public void releaseLock() {
String lockPath = WORKSPACE + "/" + lockName;
try {
if (Objects.nonNull(zkClient.checkExists().forPath(lockPath))) {
zkClient.delete().forPath(lockPath);
System.out.println(" release lock successfully! ");
}
} catch (Exception e) {
e.printStackTrace();
System.out.println(" release lock failure! ");
}
}
}
- 完整项目源码Github链接
获取源码请点我!
三、小结
- Zookeeper实现分布式锁的方案中,我们是通过创建临时节点成功与否作为能够抢占分布式锁的判断依据。
- 在分布式系统中,使用Zookeeper实现的分布式锁有且仅有一个线程能够抢到分布式锁(即:有且仅有一个线程能够在指定path下成功创建zk临时节点)
- 未能获得分布式锁的线程理应进入等待状态。有2种情形可能获取不到分布式锁:1)多个线程正常参与分布式锁竞争,已有某个线程成功创建临时接点;2)创建临时节点的过程发生异常。