前言
实现分布式锁可以利用Redis、Zookeeper、MySQL等中间件,其中Zookeeper最为适合,主要是因为两点:
- 其自带节点变更监听机制,可以在锁释放后第一时间通知其他等待的客户端,比起让客户端不断重试这种方案,它更高效,CPU占用率更低
- 在部署多个节点避免单点故障时,也能很好的保证数据一致性
本文主要探讨的是利用Zookeeper实现的分布式排他锁,共享锁目前不在讨论范围内。
原理
定义锁
通过数据节点来表示一个锁,例如/exclusive_lock/lock
节点就可以被定义为一个锁
获取锁
在需要获取排他锁时,所有客户端都会试图通过调用create接口,在/exclusive_lock
节点下创建临时子节点/exclusive_lock/lock
。Zookeeper会保证在所有的客户端中,最终只有一个客户端能够创建成功,创建成功的客户端就获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock
节点上注册一个子节点变更的Watcher监听,以便实时监听到lock
节点的变更情况
释放锁
因为/exclusive_lock/lock
是一个临时节点,因此在以下两种情况下,都有可能释放锁。
- 正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除
- 当前获取锁的客户端挂掉了,临时节点在超时后会被Zookeeper移除
无论在什么情况下移除了lock
节点,Zookeeper都会通知所有在/exclusive_lock
节点上注册了子节点变更Watcher监听的客户端,这些客户端在接收到通知后,再次重新发起分布式锁获取。
实战
以下代码使用Curator框架中的分布式锁功能,Curator是Netflix公司开源的一套Zookeeper客户端框架。首先添加依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
下面是一段没有加锁的生成ID的演示代码:
public static void generateId() {
final CountDownLatch cdl = new CountDownLatch(1);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss|SSS");
String id = formatter.format(LocalDateTime.now());
System.out.println(id);
}).start();
}
cdl.countDown();
}
运行上述代码生成的ID,发现ID会重复。使用分布式锁,再次运行,ID不会重复:
/**
* 获取客户端实例
* @return 客户端实例
*/
public static CuratorFramework getClient() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
return CuratorFrameworkFactory.newClient("192.168.7.81:2181", 5000, 3000, retryPolicy);
}
public static void testLock() {
String lockPath = "/exclusive_lock";
CuratorFramework client = getClient();
client.start();
final InterProcessMutex lock = new InterProcessMutex(client, lockPath);
final CountDownLatch cdl = new CountDownLatch(1);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
cdl.await();
lock.acquire(); // 加分布式锁
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss|SSS");
String id = formatter.format(LocalDateTime.now());
System.out.println(id);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
lock.release(); // 释放分布式锁
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
cdl.countDown();
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
client.close();
}