在 JDK 的 java.util.concurrent.locks
中,为我们提供了可重入锁,读写锁,及超时获取锁的方法。 为我们提供了完好的支持,但是在分布式系统中,当多个应用需要共同操作某一个资源时。 我么就无法使用 JDK 来实现了,这时就需要使用一个外部服务来为此进行支持,现在我们选用 ZooKeeper + Curator 来完成分布式锁
项目环境
- ZooKeeper 3.5.3-beta
- Curator 4.0.0
如果 ZooKeeper 版本为 3.4.x,请进行兼容处理
准备工作
下载、安装、启动 ZooKeeper,可以查看这篇博文 ZooKeeper 的安装、配置、启动和使用(一)——单机模式
如果想跳过这一步的话请参考最下面的便捷测试
创建一个 Maven 工程,然后引入所需资源1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
在 src/test/java 下创建一个 DistributedLockDemo 类
基本代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public class DistributedLockDemo {
// ZooKeeper 锁节点路径, 分布式锁的相关操作都是在这个节点上进行
private final String lockPath = "/distributed-lock";
// ZooKeeper 服务地址, 单机格式为:(127.0.0.1:2181), 集群格式为:(127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183)
private String connectString;
// Curator 客户端重试策略
private RetryPolicy retry;
// Curator 客户端对象
private CuratorFramework client;
// client2 用户模拟其他客户端
private CuratorFramework client2;
// 初始化资源
public void init() throws Exception {
// 设置 ZooKeeper 服务地址为本机的 2181 端口
connectString = "127.0.0.1:2181";
// 重试策略
// 初始休眠时间为 1000ms, 最大重试次数为 3
retry = new ExponentialBackoffRetry(1000, 3);
// 创建一个客户端, 60000(ms)为 session 超时时间, 15000(ms)为链接超时时间
client = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
client2 = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
// 创建会话
client.start();
client2.start();
}
// 释放资源
public void close() {
CloseableUtils.closeQuietly(client);
}
}
共享锁
1 |
|
共享可重入锁
1 | public void sharedReentrantLock() throws Exception { |
共享可重入读写锁
1 |
|
测试结果如下:
写入数据 1
写入数据 2
读取数据 2
写入数据 3
读取数据 3
写入数据 4
读取数据 4
读取数据 4
写入数据 5
读取数据 5
读取数据线程总是能看到最新写入的数据
共享信号量
1 |
|
多重共享锁
1 |
|
Curator 分布式锁解决的问题
分布式锁服务宕机,ZooKeeper 一般是以集群部署,如果出现 ZooKeeper 宕机,那么只要当前正常的服务器超过集群的半数,依然可以正常提供服务
持有锁资源服务器宕机,假如一台服务器获取锁之后就宕机了,那么就会导致其他服务器无法再获取该锁。 就会造成 死锁 问题,在 Curator 中,锁的信息都是保存在临时节点上,如果持有锁资源的服务器宕机,那么 ZooKeeper 就会移除它的信息,这时其他服务器就能进行获取锁操作
便捷测试
为了测试上面的代码,我们需要下载、安装、启动一个 ZooKeeper 服务,然后将该服务地址配置为 connectString。 如果更换环境的话又需要重新安装,未免麻烦了点。 Curator 为我们提供一个专门用于开发、测试的便捷方法,让我们更加专注于编写与 ZooKeeper 相关的程序。首先需要导入 curator-test 测试包1
2
3
4
5
6<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-test</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
在这个包中为我们提供了一个 TestingServer 类,主要用法如下
构造方法有多个,但是主要使用到的有这两个1
2TestingServer()
TestingServer(int port, File tempDirectory)
port 为端口
tempDirectory 为临时的 dataDir 目录
如果调用 TestingServer()
方法构造,会获取一个空闲端口,同时在 java.io.tmpdir
创建一个临时目录当作本次的 dataDir
目录
然后使用以下方法创建客户端1
2
3TestingServer server=new TestingServer();
// server.getConnectString() 方法会返回可用的服务链接地址, 如: 127.0.0.1:2181
CuratorFramework client=CuratorFrameworkFactory.newClient(server.getConnectString(), retry);
另外在测试完成记得进行资源释放1
2
3
4
5
public void close() {
CloseableUtils.closeQuietly(client);
CloseableUtils.closeQuietly(server);
}
TestingServer 能为我们简单的启动一个 ZooKeeper 服务器,但是如果需要进行集群测试呢?这个时候我们可以使用 TestingCluster 启动 ZooKeeper 集群
TestingCluster 同样提供多个构造器,但是主要使用以下两个1
2TestingCluster(int instanceQty)
TestingCluster(InstanceSpec... specs)
instanceQty 是集群的数量
specs 是 InstanceSpec 的变长参数
InstanceSpec 的创建方法可以参考 TestingServer 的构造方法实现
然后创建客户端使用以下方法1
2
3TestingCluster server=new TestingCluster(3);
// server.getConnectString() 方法会返回可用的服务链接地址, 如: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
CuratorFramework client=CuratorFrameworkFactory.newClient(server.getConnectString(), retry);
同样请记得释放资源