文章目录
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。分布式锁的实现方式有很多种,比如 Redis
、数据库 、zookeeper
等。这篇文章主要介绍用 Zookeeper
实现分布式锁。
Zookeeper 分布式锁实现原理
先说结论:Zookeeper 是基于临时顺序节点以及 Watcher 监听器机制实现分布式锁的。
(1)ZooKeeper 的每一个节点都是一个天然的顺序发号器。
在每一个节点下面创建临时顺序节点(EPHEMERAL_SEQUENTIAL)类型,新的子节点后面会加上一个次序编号,而这个生成的次序编号是上一个生成的次序编号加一。
例如,有一个用于发号的节点 “/test/lock” 为父节点,可以在这个父节点下面创建相同前缀的临时顺序子节点,假定相同的前缀为 “/test/lock/seq-”。第一个创建的子节点基本上应该为 /test/lock/seq-0000000001,下一个节点则为 /test/lock/seq-0000000002,依次类推。
(2)ZooKeeper 节点的递增有序性可以确保锁的公平。
一个 ZooKeeper 分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT 类型),然后每个要获得锁的线程都在这个节点下创建一个临时顺序节点,该节点是按照创建的次序依次递增的。
为了确保公平,可以简单的规定:编号最小的那个节点表示获得了锁。所以,每个线程在尝试占用锁之前,首先判断自己是序号是不是当前最小,如果是则获取锁。
(3)ZooKeeper 的节点监听机制,可以保障占有锁的传递有序而且高效。
每个线程抢占锁之前,先尝试创建自己的 ZNode。同样,释放锁的时候需要删除创建的 Znode。创建成功后,如果不是序号最小的节点,就处于等待通知的状态。每一个等通知的 Znode 节点,需要监视(watch)序号在自己前面的那个 Znode,以获取其删除事件。只要上一个节点被删除了,就进行再一次判断,看看自己是不是序号最小的那个节点,如果是,自己就获得锁。就这样不断地通知后一个 ZNode 节点。
另外,ZooKeeper 的内部优越的机制,能保证由于网络异常或者其他原因,集群中占用锁的客户端失联时锁能够被有效释放。什么机制呢,就是临时顺序节点。一旦占用 Znode 锁的客户端与 ZooKeeper 集群服务器失去联系,这个临时 Znode 也将自动删除。排在它后面的那个节点,也能收到删除事件,从而获得锁。
也正是这个原因,
zk
中不需要向redis
那样考虑锁可能出现的无法释放的问题了,因为当客户端挂了,节点也挂了,锁也释放了。
(四)ZooKeeper 的节点监听机制,能避免羊群效应。
ZooKeeper 这种首尾相接、后面监听前面的方式,可以避免羊群效应。所谓羊群效应就是一个节点挂掉,所有节点都去监听,然后做出反应,这样会给服务器带来巨大压力。有了临时顺序节点以及节点监听机制,当一个节点挂掉,只有它后面的那一个节点才做出反应。
具体流程
- 一把分布式锁通常使用一个 Znode 节点表示;如果锁对应的 Znode 节点不存在,首先创建 Znode 节点。这里假设为
/test/lock
,代表了一把需要创建的分布式锁。 - 抢占锁的所有客户端,使用锁的 Znode 节点的子节点列表来表示;如果某个客户端需要占用锁,则在
/test/lock
下创建一个临时顺序的子节点。比如,如果子节点的前缀为/test/lock/seq-
,则第一次抢锁对应的子节点为/test/lock/seq-000000001
,第二次抢锁对应的子节点为/test/lock/seq-000000002
,以此类推。 - 当客户端创建子节点后,需要进行判断:自己创建的子节点,是否为当前子节点列表中序号最小的子节点。如果是,则加锁成功;如果不是,则监听前一个 Znode 子节点变更消息,等待前一个节点释放锁。
- 一旦队列中的后面的节点,获得前一个子节点变更通知,则开始进行判断,判断自己是否为当前子节点列表中序号最小的子节点,如果是,则认为加锁成功;如果不是,则持续监听,一直到获得锁。
- 获取锁后,开始处理业务流程。完成业务流程后,删除自己的对应的子节点,完成释放锁的工作,以方面后继节点能捕获到节点变更通知,获得分布式锁。
代码实现
Curator 是Netflix公司开源的一套 ZooKeeper Java客户端框架,相比于 Zookeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。
这里使用 Curator 作为 Zookeeper 的客户端实现。需要先导入依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
<version>5.2.1</version>
</dependency>
客户端创建工厂类
public class ClientFactory {
//连接地址
private static final String connectionString = "127.0.0.1:2181";
//等待事件的基础单位,单位毫秒
private static final int BASE_SLEEP_TIME = 1000;
//最大重试次数
private static final int MAX_RETRIES = 3;
private static volatile CuratorFramework zkClient;
public static CuratorFramework getClient() {
//单例
if (zkClient == null) {
synchronized (ClientFactory.class) {
if (zkClient == null) {
createSimple();
}
}
}
return zkClient;
}
public static void createSimple() {
//重试策略: 第一次重试等待1秒,第二次重试等待2秒,第三次重试等待4秒
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(BASE_SLEEP_TIME, MAX_RETRIES);
zkClient = CuratorFrameworkFactory.newClient(connectionString, retryPolicy);
zkClient.start();
}
public static void createWithOptions(int connectionTimeoutMs, int sessionTimeoutMs) {
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(BASE_SLEEP_TIME, MAX_RETRIES);
zkClient = CuratorFrameworkFactory