ZooKeeper 实现分布式锁
1. 前言
在我们的应用中,经常会碰见多个请求去访问同一个资源的情况。如果请求 A 拿到这个资源数据,想要对它进行修改,但是还没有进行事务提交,此时请求 B 访问这个资源就会拿到修改前的数据,很显然请求 B 拿到的是历史数据,是不正确的。
在单个服务器的应用中,我们可以使用系统的线程来对这个资源进行加锁。那么在分布式环境中我们有什么方案来解决这个问题呢?答案就是使用分布式锁。那么什么是分布式锁?分布式锁又是如何实现的呢?本节我们就来讲解如何使用 Zookeeper 实现分布式锁,以及它的实现原理。
2. 分布式锁
在讲解 Zookeeper 实现的分布式锁之前,我们先来了解什么是分布式锁,分布式锁的实现技术,以及分布式锁常用的类型。
2.1 分布式锁的特点
顾名思义,分布式锁就是实现在分布式网络环境中的锁。也就是说,在锁的基础上加上分布式的特性,我们来分析一下分布式锁实现的必要条件:
- 在分布式环境中,多个进程对资源的访问必须具有顺序性;
- 获取锁和释放锁的过程需要高可用和高性能;
- 具有锁失效的机制,避免死锁;
- 非阻塞的锁,没有获取到锁直接返回获取锁失败。
介绍了分布式锁的特点,那么有哪些技术能够实现分布式锁呢?
2.2 分布式锁的实现技术
- Memcached: 使用
add
命令来添加key
,key
添加成功说明当前无人使用此key
,也就是说无人使用此资源,相当于获取锁。再次使用add
命令来添加相同的key
时,此时key
已存在就会添加失败,说明有人已经使用了这个key
,也就是说此资源被人占用,相当于获取锁失败; - Redis: 使用
setnx
命令来添加key
,key
添加成功说明当前无人使用此key
,也就是说无人使用此资源,相当于获取锁。再次使用setnx
命令来添加相同的key
时,此时key
已存在就会添加失败,说明有人已经使用了这个key
,也就是说此资源被人占用,相当于获取锁失败; - Chubby: Google 使用 Paxos 一致性算法实现的粗粒度分布式锁;
- Zookeeper: 使用 Zookeeper 临时顺序节点的特性,实现分布式锁和锁的等待队列。
介绍了分布式锁的实现技术,接下来我们来介绍分布式锁常用的类型。
2.3 分布式锁常用的类型
分布式锁常用的类型有两种:一种是排他锁,一种是共享锁。接下来我们分别介绍这两锁的特点。
- 排他锁
排他锁也叫独占锁,顾名思义,也就是对资源进行独占。排他锁只允许获取了该锁的线程,对具有排他锁的资源进行访问,无论是写操作还是读操作,直到该线程主动释放掉排他锁。
- 共享锁
共享锁也就是把资源进行共享,当然共享的只有读操作。共享锁只对写操作进行加锁,其它线程的读操作不做加锁操作,这样的共享机制提高了对资源访问的性能。
介绍完分布式锁的常用类型,接下来我们开始学习如何使用 Zookeeper 实现分布式锁。
3. Zookeeper 实现分布式锁
上面我们提到,Zookeeper 是根据它的临时顺序节点来实现的分布式锁,这里我们来回顾一下临时顺序节点的特性。
3.1 临时顺序节点
临时顺序节点:
- 节点具有临时性,创建该节点的 Zookeeper 客户端与 Zookeeper 服务端断开连接时,该节点会自动被 Zookeeper 服务端删除;
- 节点具有顺序性,创建该节点时,Zookeeper 服务端会根据创建时间的顺序在该节点名称后面加上顺序编号。
回顾了临时顺序节点的特性,接下来我们就使用 Zookeeper 的 Java 客户端 Curator 来创建临时顺序节点,我们可以使用在 Zookeeper Curator 一节创建的 Spring Boot 测试项目来进行测试。
我们可以在测试类 CuratorDemoApplicationTests 中编写测试用例:
@SpringBootTest
class CuratorDemoApplicationTests {
@Autowired
private CuratorService curatorService;
@Test
void contextLoads() throws Exception {
// 获取客户端
CuratorFramework client = curatorService.getCuratorClient();
// 开启会话
client.start();
// 第一次创建临时顺序节点
String s1 = client.create()
// 如果有父节点会一起创建
.creatingParentsIfNeeded()
// 节点类型:临时顺序节点
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
// 节点路径 /wiki
.forPath("/wiki-");
// 输出
System.out.println(s1);
// 第二次创建临时顺序节点
String s2 = client.create()
// 如果有父节点会一起创建
.creatingParentsIfNeeded()
// 节点类型:临时顺序节点
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
// 节点路径 /wiki
.forPath("/wiki-");
// 输出
System.out.println(s2);
// 关闭客户端
client.close();
}
}
执行测试方法,控制台输出:
/wiki-0000000000
/wiki-0000000001
我们可以发现,控制台一共输出了两个 /wiki 节点,而且每个 /wiki 节点后面都增加了编号,此时我们去 zkCli 命令行客户端查看所有节点,发现并没有 /wiki 节点。因为在我们的测试程序中,我们关闭了客户端,所以临时节点会被移除。
Tips: 如果这里创建失败,请同学们注意父节点是否存在 ACL 访问控制。
回顾了临时顺序节点,那么如何使用 Zookeeper 的临时顺序节点来实现分布式锁呢?接下来我们就开始介绍如何使用 Zookeeper 的临时顺序节点来控制它们的访问顺序。
3.2 分布式锁实现
本节我们来介绍分布式锁实现的具体步骤:
- 创建临时顺序节点: 每一次获取资源的请求,我们都需要使用 Zookeeper 客户端创建一个临时顺序节点,用这个临时顺序节点在 Zookeeper 服务端中获取锁。
- 获取锁: 这里的锁并不具体指代什么,而是根据 Zookeeper 的临时顺序节点的顺序来决定是否获取了锁。如果该节点的顺序编号是最小的,则说明该节点是排在最前面的,在它之前无人占领资源,也就可以说该节点获取了锁,具有访问资源的权限。
- 监听锁: 如果获取锁这一步发现 Zookeeper 客户端创建的临时顺序节点的顺序编号不是最小的,也就是在这个临时顺序节点之前存在其它临时顺序节点,那么就可以说这个节点获取锁失败了,它会进入等待队列。我们可以监听它的前一个节点,只要它的前一个临时顺序节点的删除事件触发,我们就可以获取临时顺序节点的列表来重新确认这个节点的顺序。
- 释放锁: 当一个请求对资源的操作结束后,我们可以使用 Zookeeper 客户端的节点删除 API 来删除这个请求创建的临时顺序节点。除了使用 API 来主动释放锁之外,根据临时顺序节点的特性,当创建这个临时顺序节点的 Zookeeper 客户端与 Zookeeper 服务端断开连接时,这个临时顺序节点会被 Zookeeper 服务端移除。这两种方式都会触发临时节点的删除事件,让下一个临时顺序节点来确认自身的顺序。
4. 总结
本节内容中,我们学习了什么是分布式锁,以及它的特点和类型,还学习了使用 Zookeeper 实现分布式锁的主要步骤。以下是本节内容的总结:
- 分布式锁的特点和常用类型。
- 临时顺序节点的特性。
- 使用 Zookeeper 实现分布式锁的主要步骤。