Fork me on GitHub

基于ZooKeeper的分布式锁

1. 原理

底层是基于 ZooKeeper 的临时顺序节点。

ZooKeeper 的节点类型可以分为持久节点、持久顺序节点、临时节点、以及临时顺序节点。在基于 Dubbo 的分布式系统中,使用 ZooKeeper 做注册中心,就是使用的临时节点。如果对 ZooKeeper 基础知识不了解的同学可以百度下相关知识点。

我们很容易理解为什么用临时节点,因为加锁就相当于临时增加一个节点,解锁就去掉这个临时节点。

那为什么是顺序节点呢?我们先来画张图,看看 ZooKeeper 做分布式锁的原理。

我们设置锁节点的父节点为 /distribute_lock/productId_lock 。distribute_lock 表示所有的分布式锁都放在这个节点下,productId_lock 表示这个是某个商品用到的锁,如果别的模块也需要用分布式锁,也可以对应创建节点。比如,用户系统可以设置为 /distribute_lock/userId_lock 。那么此时,ZooKeeper 的节点如下图所示。

注意: 这里的 distribute_lock 和 productId_lock 这两个节点是持久节点。我们最终是在 productId_lock 节点下创建临时节点。

如果这个时候多个线程来同时加锁,会怎么样呢?ZooKeeper 的底层保证同一时间只有一个线程能创建临时节点,这一点类似于 Redis 的单线程。当其他线程来创建节点发现已经有一个临时节点了,就会等着。图就会变成下面这个样子。来,上图:

如果不用顺序节点,没有创建节点成功的线程都会等着,等到线程1完成业务,解锁之后,其他所有的线程都会一拥而上去创建自己的节点。

我们回忆一下,使用 Redission 分布式锁的时候是不是就是这样。一个线程加锁成功之后,其他的线程就等着,在死循环里不断尝试去加锁。但是这里有个问题,Redis 可是单机能抗 10 万并发的,而 ZooKeeper 是没有这么高性能的。所以如果这里等待的线程过多,当线程1删除节点解锁之后,其他所有线程同时争抢着创建节点,ZooKeeper 未必抗的住。所以,引入了顺序节点。

如果使用临时顺序节点会怎样呢?来,上图:

如上图,线程1表示第一个加锁成功的线程,线程2表示加锁失败的线程。如果使用的是临时顺序节点,上来不管三七二十一,先创建一个临时节点再说。然后再判断自己创建的子节点是不是第一个子节点,如果是,证明加锁成功,如果不是,加锁失败。加锁失败的线程会创建一个监听器,用来监听自己的上一个节点

这样,线程2加锁失败,监听线程1;线程3加锁失败,监听线程2;以此类推。。。

那使用临时顺序节点,解锁的时候又会发生什么呢?线程1加锁成功后,执行自身的业务,执行完毕之后删除临时节点1,同时通知线程2,此时线程2就会去加锁。而此时临时节点1已经被删除了,线程2创建的临时节点就是第一个子节点,就会加锁成功。至于后面的线程,以此类推即可。

在使用临时顺序节点之后,解锁的时候就不会造成很多线程同时去争抢着创建节点了。相当于一个一个的线程排着队加锁了。这个实现的就是公平锁。哪个线程先来,就排队排在前面。

思考:我们在讲 Redis 分布式锁的时候提到了一个续期的概念,ZooKeeper 有类似的概念吗?

答:ZooKeeper 没有续期的概念,但是 ZooKeeper 的客户端和服务端在建立连接之后会保持心跳,只要服务端确认客户端之间的通信正常,就不会删除节点。

再思考:如果 ZooKeeper 客户端在执行自身业务的过程中挂掉了,没来得及删除节点,会怎么样?

答:因为客户端和服务端会保持心跳,如果客户端挂了,无法向服务端发送心跳,服务端过段时间就会自动删除节点。这一点和 Redis 分布式锁设置的过期时间实现的结果是一样的,不会造成死锁。

2. Curator实现

在我们弄清楚了加锁、争抢锁、解锁之后,我们来看看实际项目中是怎样使用 ZooKeeper 做分布式锁的。其实和 Redis 分布式锁一样,一般也是使用封装好的框架,不用自己手撸代码。一般使用 Curator 客户端实现。接下来简单了解下 Curator 。

学习第一步,官网来探路。建议看这篇文章的时候打开 Curator 的官网。我们直接看 Getting Started ,这里给出了 Curator 实现分布式锁的方案。

这里要注意 Curator 版本和 ZooKeeper 服务器版本的对应关系,否则会报异常。

org.apache.zookeeper.KeeperException$UnimplementedException:

我的虚拟机上 ZooKeeper 版本是3.4.14 ,按照Curator 官网上的指导选择合适的 Curator 版本。官网上有这样一段话。

Curator 4.0 supports ZooKeeper 3.4.x ensembles in a soft-compatibility mode. To use this mode you must exclude ZooKeeper when adding Curator to your dependency management tool.

在这句的意思大致上,如果使用 Curator 4.0 ,需要在 maven 依赖中移除掉 ZooKeeper 。官方给的示例如下:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>${curator-version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>

紧接着,下面又有这么一句话。

You must add a dependency on ZooKeeper 3.4.x also.

提示我们需要手动添加 ZooKeeper 3.4.x 的依赖,我这里选择的是 3.4.14 。最终依赖如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</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.14</version>
</dependency>

这样,我们的依赖就配置好了。接下来,我们直接拿官网上的示例来用。

来,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CuratorTest {
public static void main(String[] args) throws Exception {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient(
"127.0.0.1:2181",
retryPolicy);
client.start();

InterProcessMutex lock = new InterProcessMutex(client, "/distribute_lock/product01_lock");
lock.acquire();
try {
// 执行业务逻辑,这里停30秒,为了看效果
Thread.sleep(30000L);
} finally {
lock.release();
}
}
}

代码极其简单,第 2 行代码是设置重试机制,第 4 ~ 7 行代码使用工厂模式创建客户端,然后启动客户端。

第 9 行代码创建一个加锁对象,这个是可重入锁,后面源码分析的时候会讲到。

第 10 行代码加锁,11 ~ 14 模拟执行业务代码,15 行解锁。

执行一次,看看效果。

这个是我使用命令行查看的节点状态,在 /distribute_lock 节点下有一个 /product01_lock 节点,我们创建的临时顺序节点就是 /product01_lock 的子节点。来,看一下:

如上图所示,有一个子节点,后面几位全是 0 ,我推测下一个子节点后几位应该是 00000000001 。上面这个截图是我在解锁之前截的,如果过了 30 秒,这个临时节点就会被删除了。

在我们实际的使用过程中,会结合 Spring 去注册 CuratorFramework 的 bean ,之后的用法就是如 9 ~ 15 代码所示。实际的业务代码放在 try 里面就行,最后 finally 里面记得一定要释放锁即可。讲到这里,ZooKeeper 做分布式锁的原理和用户就大多讲完了。

下一篇,我们讲 ZooKeeper 分布式锁源码,Curator实现ZooKeeper分布式锁源码

本文标题:基于ZooKeeper的分布式锁

原始链接:https://zhaoxiaofa.com/2019/08/30/基于ZooKeeper的分布式锁/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。