前言
前面并发章节中提到各种锁来防止并发带来的异常结果,但是这都是在单机情况下才奏效,如果在分布式情况下,就要采用分布式锁了,分布式锁的实现有很多种,主要有zookeeper和redis分布式锁,此章先说说zookeeper怎么实现分布式锁。
分布式锁实现
前面提到zookeeper的搭建和客户端操作,我们在这基础上来实现一下分布式锁。
zookeeper主要是各种节点,要实现分布式锁可以用创建节点和删除节点来实现,但是创建什么样的节点呢?前面提到zookeeper有四种节点,其中的顺序节点就可以模拟各个线程,每个客户端在同一个根节点下创建顺序节点时,都会创建一个唯一编号的顺序节点,不会重复。而且zookeeper中的临时顺序节点,在客户端断开连接时,就会自动此客户端创建的顺序节点,这就有效防止了死锁的产生。
而且zookeeper中的监听机制可以用来实现释放锁通知其他线程的作用。
下面我们来理一下实现逻辑:定义一个分布式锁的根节点,要想获取锁就要在根节点下创建临时顺序节点,然后看看自己创建的是否是根节点下所有子节点中的第一个(编号最小),如果是第一个就说明获取锁成功,如果不是则获取失败,这是就要加一个监听器,来监听比自己小的前一个节点(只监听前一个,防止羊群效应,如果监听所有的,那一个线程释放锁,就会通知所有其他线程,这样对服务器压力太大;如果只监听前一个,只有前一个节点删除才会得到通知。),如果前一个节点删除了,就会通知自己,这时自己就是最小的节点,说明获取到锁,以此类推,这样分布式锁就实现了,通过上面描述,可以知道zookeeper实现的是公平锁。
代码实现
public class ZKLock {
private static final String LOCK_PREFIX = "/demo/lock/lock-";
private static final String PARENT_PATH = "/demo/lock";
private String currentPath;
private String prePath;
private CuratorFramework client;
public ZKLock() {
ExponentialBackoffRetry retry = new ExponentialBackoffRetry(1000, 3);
client = CuratorFrameworkFactory.builder()
.connectString("123.206.212.170")
.connectionTimeoutMs(10000)
.sessionTimeoutMs(10000)
.retryPolicy(retry).build();
client.start();
}
public static void main(String[] args){
ZKLock zkLock = new ZKLock();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
zkLock.lock();
System.out.println(Thread.currentThread().getName() + "获取锁成功");
zkLock.unLock();
System.out.println(Thread.currentThread().getName() + "释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
public synchronized boolean lock() throws Exception{
// 尝试获取锁
boolean lock = tryLock();
if (lock) {
return true;
}
//获取不到,等待其他线程释放锁
print("获取锁失败,进入等待");
waitPre();
print("等待结束,获取到锁");
return true;
}
private boolean tryLock() throws Exception{
// 创建临时顺序节点
currentPath = client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(LOCK_PREFIX);
print("当前节点:" + currentPath);
// 创建的节点是否是所有子节点中的第一个
List<String> list = sortChilds();
print("所有子节点:" + JSONArray.toJSONString(list));
if (list == null) {
throw new Exception("创建失败");
}
// 是第一个就说明获取到锁,不是则未获取到锁
return currentPath.equals(list.get(0));
}
/**
* 释放锁
* @throws Exception
*/
public void unLock() throws Exception {
client.delete().forPath(currentPath);
}
/**
* 等待上一个节点删除
* @throws Exception
*/
private void waitPre() throws Exception{
// 上一个节点
prePath = prePath();
// 监听上一个节点变化
CountDownLatch countDownLatch = new CountDownLatch(1);
client.getData().usingWatcher(new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
print("上一个节点删除了");
countDownLatch.countDown();
}
}).forPath(prePath);
countDownLatch.await();
}
/**
* 获得所有子节点,并排序
* @return
* @throws Exception
*/
private List<String> sortChilds() throws Exception {
List<String> list = client.getChildren().forPath(PARENT_PATH);
if (list != null) {
list = list.stream().map(e -> e = PARENT_PATH + "/" + e).collect(Collectors.toList());
Collections.sort(list);
}
return list;
}
private String prePath() throws Exception{
List<String> list = sortChilds();
int i = list.indexOf(currentPath);
return list.get(i - 1);
}
private void print(String s) {
System.out.println(Thread.currentThread().getName() + " " + s);
}
}
上面是我们自己手动实现的分布式锁,只是简单示例,在真正使用的时候,建议使用zookeeper客户端Curator自带的分布式锁,下面是客户端实现分布式锁的简单示例:
public class CuratorLock {
private CuratorFramework client;
public CuratorLock() {
ExponentialBackoffRetry retry = new ExponentialBackoffRetry(1000, 3);
client = CuratorFrameworkFactory.builder()
.connectString("123.206.212.170")
.connectionTimeoutMs(10000)
.sessionTimeoutMs(10000)
.retryPolicy(retry).build();
client.start();
}
public void test() throws Exception{
List<String> list = new ArrayList<>();
list.add("/demo/curatorLock");
final InterProcessMultiLock lock = new InterProcessMultiLock(client, list);
int num = 10;
CountDownLatch downLatch = new CountDownLatch(num);
for (int i = 0; i < num; i++) {
new Thread(() -> {
try {
lock.acquire();
System.out.println(Thread.currentThread().getName()+"获得锁");
Thread.sleep(1000);
lock.release();
downLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
downLatch.await();
}
public static void main(String[] args) throws Exception{
CuratorLock demo = new CuratorLock();
demo.test();
}
}
总结
一般公司里都会有现成的zookeeper分布式集群,在平时要使用到分布式锁的时候,可以使用现有的zookeeper集群来实现,但是在高并发的情况下,还是建议使用redis分布式锁,因为zookeeper分布式锁是用节点的创建和删除来实现的,我们都知道,zookeeper中创建和删除节点都是在leader节点上执行的,而leader还要将数据同步到各个follower节点,这样频繁交互是很消耗性能的。
因此在高并发要求不高的情况下推荐使用zookeeper分布式锁,因为他的高可用性。而对并发和性能要求很高的时候,推荐使用redis分布式锁。