基于 ZooKeeper 的分布式锁实现

ZK 基本概念

  • apache hadoop 下面的子项目,是一个树形目录服务
  • 字面意思就是动物管理员,诞生之初用来管理 hadoop(大象)、Hive(蜜蜂)、Pig(小猪)
  • 用于分布式应用的协调服务
  • 提供的主要常用功能:1 集群管理(注册中心) 2 分布式锁 3 配置管理(配置中心)

ZK 数据结构

跟 Unix 文件系统非常类似,可以看做是一颗,每个节点叫做 znode,每一个节点可以通过路径来标识,znode 节点分为两种类型:

  • 持久节点:该数据节点被创建后,就会一直存在于 ZooKeeper 服务器上,直到有删除操作来主动删除这个节点,详细来说还可以分为普通持久节点和带顺序号的持久节点。
  • 临时节点:临时节点的生命周期和客户端会话绑定在一起,客户端会话失效,则这个节点就会被自动清除,同样分为普通临时节点和带顺序号的临时节点。

统一配置管理

我们可以将 common.yml 这份配置放在 ZooKeeper 的 znode 节点中,系统 A、B、C 监听该 znode 节点有无变更,如果变更了,及时响应。

统一命名服务

系统 A、B、C 通过访问 /myMachine 这个 znode 节点就能拿到对应的 IP 地址

分布式锁实现

利用 Zookeeper 可以创建带顺序号的临时节点的特性来实现分布式锁,系统 A、B、C 都去访问 /locks 节点,访问的时候会创建带顺序号的临时节点,比如,系统 A 创建了 id_000000 节点,系统 B 创建了 id_000002 节点,系统 C 创建了 id_000001 节点。

接着,拿到 /locks 节点下的所有子节点(id_000000, id_000001, id_000002),判断自己创建的是不是最小的那个节点:

  • 是,则拿到锁(执行完操作后,删掉创建的节点,即表示释放锁)
  • 否,则监听比自己要小 1 的节点变化

举个例子:

1 系统 A 拿到 /locks 节点下的所有子节点,经过比较,发现自己(id_000000),是所有子节点最小的,所以得到锁
2 系统 B 拿到 /locks 节点下的所有子节点,经过比较,发现自己(id_000002),不是所有子节点最小的。所以监听比自己小 1 的节点 id_000001 的状态
3 系统 C 拿到 /locks 节点下的所有子节点,经过比较,发现自己(id_000001),不是所有子节点最小的。所以监听比自己小 1 的节点 id_000000 的状态
……
等到系统 A 执行完操作以后,将自己创建的节点删除(id_000000)。通过监听,系统 C 发现id_000000 节点已经删除,发现自己已经是最小的节点,于是顺利拿到锁。

实践:ZK 客户端框架 Curator

Curator 是 Netflix 公司开源的一套 ZooKeeper 客户端框架,解决了很多 ZooKeeper 客户端非常底层的细节开发工作,包括连接重连、反复注册 Watcher 和NodeExistsException 异常等。

public class InterprocessLock {
    public static void main(String[] args)  {
        CuratorFramework zkClient = getZkClient();
        String lockPath = "/lock";
        InterProcessMutex lock = new InterProcessMutex(zkClient, lockPath);
        // 模拟50个线程抢锁
        for (int i = 0; i < 50; i++) {
            new Thread(new TestThread(i, lock)).start();
        }
    }
 
 
    static class TestThread implements Runnable {
        private Integer threadFlag;
        private InterProcessMutex lock;
 
        public TestThread(Integer threadFlag, InterProcessMutex lock) {
            this.threadFlag = threadFlag;
            this.lock = lock;
        }
 
        @Override
        public void run() {
            try {
                lock.acquire();
                System.out.println("第"+threadFlag+"线程获取到了锁");
                // 等到1秒后释放锁
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    lock.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
 
    private static CuratorFramework getZkClient() {
        String zkServerAddress = "192.168.3.39:2181";
        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3, 5000);
        CuratorFramework zkClient = CuratorFrameworkFactory.builder()
                .connectString(zkServerAddress)
                .sessionTimeoutMs(5000)
                .connectionTimeoutMs(5000)
                .retryPolicy(retryPolicy)
                .build();
        zkClient.start();
        return zkClient;
    }
}

锁的释放

创建的节点为临时会话顺序节点(EPHEMERAL_SEQUENTIAL),即该节点会在客户端链接断开时被删除,也可以手动 release 锁删除该节点。

可重入性

ZooKeeper 的锁是可重入的,即同一个线程可以多次获取锁,只有第一次真正的去创建临时会话顺序节点,后面的获取锁都是对重入次数加 1。相应的,在释放锁的时候,前面都是对锁的重入次数减 1,只有最后一次才是真正的去删除节点。

客户端故障检测

客户端会在会话的有效期内,向服务器端发送 PING 请求进行心跳检查,证明自己还存活。服务器端接收到客户端的请求后,会进行对应的客户端会话激活,会话激活就是延长该会话的存活期。如果有会话一直没有激活,那么说明该客户端出问题了,服务器端的会话超时检测任务就会检查出那些一直没有被激活的与客户端的会话,然后对其进行清理,清理中有一步就是删除临时会话节点(包括临时会话顺序节点)。这就保证了 ZooKeeper 分布锁的容错性,不会因为客户端的意外退出,导致锁一直不释放,其他客户端获取不到锁。

数据一致性

ZooKeeper 服务器集群一般由一个 leader 节点和其他的 follower 节点组成,数据的读写都是在 leader 节点上进行。当一个写请求过来时,leader 节点会发起一个 proposal,
待大多数 follower 节点都返回 ack 之后,才允许客户端 commit,待大多数 follower 节点都对这个 proposal 进行 commit 了,leader 才会对客户端返回请求成功;如果之后leader 挂掉了,那么会采用 leader 选举算法 zab 协议保证存在最新数据的 follower 节点当选为新的 leader。所以,新的 leader 节点上都会有原来 leader 节点上提交的所有数据,如此保证客户端请求数据的一致性。

posted @ 2022-07-09 18:09  这个杀手冷死了  阅读(189)  评论(0编辑  收藏  举报