Zookeeper 随笔(一):使用 Curator 框架实现 ZooKeeper 分布式锁

实现分布式锁目前有三种流行方案,分别为基于数据库、Redis、Zookeeper 的方案,我们来看下使用 Zookeeper 如何实现分布式锁。

在 ZooKeeper 中,可以通过在 ZooKeeper 中创建一个数据节点来表示一个锁。比如,/exclusive_lock/lock 节点就可以表示为一个锁。

在需要获取排他锁时,所有的客户端都会试图通过 create() 接口,在 /exclusive_lock 节点下创建临时的子节点 /exclusive_lock/lock,但 ZooKeeper 的强一致性最终只会保证仅有一个客户单能创建成功,那么就认为该客户端获取了锁。同时,所有没有获取锁的客户端事务只能处于等待状态,这些处于等待状态的客户端事先可以在 /exclusive_lock 节点上注册一个子节点变更的 Watcher 监听,以便实时监听到子节点的变更情况。

我们已经提到 /exclusive_lock/lock 是一个临时节点,因此在以下两种情况下可能释放锁。

  • 当前获取锁的客户端发生宕机,那么 ZooKeeper 服务器上保存的临时性节点就会被删除;
  • 正常执行完业务逻辑后,由客户端主动来将自己创建的临时节点删除。

无论什么情况下,临时节点 /exclusive_lock/lock 被移除,ZooKeeper 都会通知在 /exclusive_lock 注册了子节点变更 Watcher 监听的客户端。这些客户端在接收到通知以后就会再次发起获取锁的操作,即重复“获取锁”过程。排他锁流程如下:

 

 

假如当前有 1000 个节点在等待锁,如果获得锁的客户端释放锁时,这 1000 个客户端都会被唤醒,非常影响服务器性能,这种情况称为“羊群效应”;在这种羊群效应中,zookeeper 需要通知 1000 个客户端,这会阻塞其他的操作,最好的情况应该只唤醒新的最小节点对应的客户端。应该怎么做呢?在设置事件监听时,每个客户端应该对刚好在它之前的子节点设置事件监听,例如子节点列表为 /lock/lock-0000000000、/lock/lock-0000000001、/lock/lock-0000000002,序号为 1 的客户端监听序号为 0 的子节点删除消息,序号为 2 的监听序号为 1 的子节点删除消息。 流程如下:

  1. 客户端连接 zookeeper,并在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推;
  2. 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;

Zookeeper 原生客户端暴露的 API 已经非常简洁了,但是实现一个分布式锁还是比较麻烦的。Curator 是 Netflix 公司开源的一个 Zookeeper 客户端,Curator 框架在 zookeeper 原生 API 接口上进行了包装,解决了很多 ZooKeeper 客户端非常底层的细节开发。提供 ZooKeeper 各种应用场景 (recipe, 比如:分布式锁服务、集群领导选举、共享计数器、缓存机制、分布式队列等) 的抽象封装,实现了 Fluent 风格的 API 接口, 是最好用,最流行的 zookeeper 的客户端。

Curator 主要从以下几个方面降低了 zk 使用的复杂性:

  • 重试机制: 提供可插拔的重试机制, 它将给捕获所有可恢复的异常配置一个重试策略,并且内部也提供了几种标准的重试策略 (比如指数补偿)
  • 连接状态监控: Curator 初始化之后会一直对 zk 连接进行监听,一旦发现连接状态发生变化将会作出相应的处理
  • zk 客户端实例管理:Curator 会对 zk 客户端到 server 集群的连接进行管理,并在需要的时候重建 zk 实例,保证与 zk 集群连接的可靠性
  • 各种使用场景支持:Curator 实现了 zk 支持的大部分使用场景(甚至包括 zk 自身不支持的场景),这些实现都遵循了 zk 的最佳实践,并考虑了各种极端情况

下面代码演示使用 Curator 框架来实现 ZooKeeper 分布式锁。

maven 引用:

<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-recipes</artifactId>
  <version>2.8.0</version>
</dependency>

示例代码:

package com.tech.spring.sharp.zookeeper;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class CuratorLock {
    public static void main(String[] args) throws Exception {
        //创建zookeeper的客户端:重试策略,初始化每次重试之间需要等待的时间,基准等待时间为1秒。
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.30.243:2181,192.168.30.244:2181,192.168.30.245:2181,192.168.30.246:2181", retryPolicy);
        client.start();
        //创建分布式锁, 锁空间的根节点路径为/curator/lock_node
        InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock_node");
        mutex.acquire();
        //获得了锁, 进行业务流程
        System.out.println("Enter mutex");
        //完成业务流程, 释放锁
        mutex.release();
        //关闭客户端
        client.close();
    }
}

  

posted @ 2022-10-18 19:37  会开窗的金莲  阅读(60)  评论(0编辑  收藏  举报