分别使用 Zookeeper 的原生 API 和 Curator 框架实现分布式锁
Zookeeper实现分布式锁
什么是分布式锁?
分布式环境下多个进程实例同时对同一个资源进行操作,为了解决这个问题,提出分布式锁;
进程访问资源,先获取锁,拿到锁代表有了对资源操作的权限了,其他没有拿到锁的进程需要等待;
这个锁可以是 Redis、Zookeeper,甚至也可以是数据库;
分布式锁的特点:
- 互斥性:任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁
- 高可用性:提供分布式锁的服务需要做到高可用,小部分机器宕机不能影响正常使用
- 防止锁超时:如果客户端没有释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或网络异常时产生死锁
- 独占性:加解锁必须由同一台机器进行,也就是谁加的锁,谁来释放,不能出现自己加的锁,但是别人释放了的情况
实现思路
整体思路:利用创建 Zookeeper 的临时有序节点,多个客户端同时创建临时有序的节点,谁的序号小(谁先创建)谁就拿到锁,其他序号依次监控自己的前一名进行等待,等待前一名释放锁,才会轮到自己;
整体过程:
- 客户端准备获取分布式锁,连接 Zookeeper 服务端,向
/locks
路径下创建一个临时的序列节点 - 获取
/locks
下所有的节点,看看自己是不是序号最小的节点:- 是:成功获取到锁,返回
- 否:获取锁失败,对自己的前一个节点进行监听,阻塞直到监控到前一个节点发生
delete
事件时,代表轮到自己了,自己获取到锁,返回
delete
释放锁
环境准备
搭建一个普通的 maven 工程,引入相关依赖:
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<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
<!-- 还需要引入 org.apache.zookeeper 不过上面已经引入了 -->
</dependencies>在项目的 resources 文件夹下创建 log4j.properties
1
2
3
4
5
6
7
8INFO, stdout =
org.apache.log4j.ConsoleAppender =
org.apache.log4j.PatternLayout =
%d %p [%c]- %m%n =
org.apache.log4j.FileAppender =
target/spring.log =
org.apache.log4j.PatternLayout =
%d %p [%c]- %m%n =
使用 Zookeeper 原生 API 实现分布式锁
主要是三个方法:
- 构造方法:在构造方法里进行连接 Zookeeper 服务端,创建根节点,监听前一个节点事件
- 获取锁(加锁):就是上述的前两步骤,创建自己的专属锁路径,然后排序看看自己是不是第一个,是第一个代表成功加锁,然后返回,不是第一个就对前一个节点进行监控,然后阻塞等待直到前一个节点被释放掉
- 释放锁(解锁):就是
delete
掉自己创建的临时锁路径
具体代码:
1 | package icu.sunnyc.zk.demo2; |
分布式锁,并发获取锁测试:
1 | package icu.sunnyc.zk.demo2; |
测试结果:
1 | 2022-05-14 21:19:22,767 INFO [icu.sunnyc.zk.demo2.DistributedLock]- pool-1-thread-3-EventThread 成功与服务器建立连接! |
日志分为三部分来看:
可以看到:
五台服务器同时与 Zookeeper 服务端建立连接
4 号线程优先拿到锁了,因为他是
/locks/seq-0000000021
序号最小的然后每个节点都监控了自己的前一个节点,等待前一个节点释放锁
1
2
3
42022-05-14 21:19:22,803 INFO [icu.sunnyc.zk.demo2.DistributedLock]- 线程:pool-1-thread-5,当前节点:seq-0000000024,已监控前一个节点:/locks/seq-0000000023
2022-05-14 21:19:22,803 INFO [icu.sunnyc.zk.demo2.DistributedLock]- 线程:pool-1-thread-1,当前节点:seq-0000000025,已监控前一个节点:/locks/seq-0000000024
2022-05-14 21:19:22,803 INFO [icu.sunnyc.zk.demo2.DistributedLock]- 线程:pool-1-thread-2,当前节点:seq-0000000023,已监控前一个节点:/locks/seq-0000000022
2022-05-14 21:19:22,803 INFO [icu.sunnyc.zk.demo2.DistributedLock]- 线程:pool-1-thread-3,当前节点:seq-0000000022,已监控前一个节点:/locks/seq-0000000021可以看到线程 3 节点
seq-0000000022
监控了/locks/seq-0000000021
,所以 4 号线程释放锁之后,3 会拿到锁可以看日志进行验证
1
2
3
42022-05-14 21:19:23,676 INFO [icu.sunnyc.zk.demo2.DistributedLock]- pool-1-thread-3-EventThread 成功与服务器建立连接!
2022-05-14 21:19:23,676 INFO [icu.sunnyc.zk.demo2.DistributedLock]- 线程 pool-1-thread-3-EventThread 监控到锁 /locks/seq-0000000021 已经被释放啦
2022-05-14 21:19:23,676 INFO [icu.sunnyc.zk.demo2.DistributedLockTest]- pool-1-thread-3 已经成功拿到锁,正在处理自己的事情
2022-05-14 21:19:25,321 INFO [icu.sunnyc.zk.demo2.DistributedLock]- 线程:pool-1-thread-3 已经释放锁 /locks/seq-0000000022依次类推,3 号线程释放锁,监控着 3 号的 2 号线程拿到锁;直到最后都处理完成
使用 Curator 框架的分布式锁
原生 API 开发存在的问题:
- 会话连接是异步的,需要自己去处理。比如使用 CountDownLatch
- Watch 需要重复注册,不然就不能生效
- 开发的复杂性还是比较高的
所以使用 Curator 实现好的分布式锁,方便且靠谱
- InterProcessMutex:分布式可重入排它锁
- InterProcessSemaphoreMutex:分布式排它锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
参考:https://www.cnblogs.com/qlqwjy/p/10518900.html
1 | package icu.sunnyc.zk.demo3; |