Zookeeper

Zookeeper是一个分布式协调服务,用于管理和协调分布式应用程序的组件。它提供了集中式的服务,用于维护配置信息、命名、分布式同步和组服务。Zookeeper可以帮助开发人员简化分布式应用的设计和实现。

Zookeeper的核心概念

  1. 节点(ZNode)

    • Zookeeper的数据模型类似于文件系统,由一棵层次化的节点树组成。
    • 每个节点称为ZNode,可以存储数据和子节点。
    • ZNode有两种类型:临时节点(Ephemeral)和持久节点(Persistent)。
      • 临时节点:会话结束时自动删除。
      • 持久节点:需要明确删除操作才能删除。
  2. 会话(Session)

    • 客户端与Zookeeper服务器之间的连接称为会话。
    • 会话是有超时时间的,客户端需要定期发送心跳来维持会话。
  3. 版本(Version)

    • 每个ZNode都有版本信息,包括数据版本(dataVersion)、子节点版本(cversion)和ACL版本(aversion)。
    • 版本信息在更新时自动递增,用于实现乐观锁机制。
  4. 监视(Watchers)

    • 客户端可以在ZNode上设置监视器,当ZNode发生变化时,客户端会收到通知。
    • 监视是一次性的,需要重新设置。

Zookeeper的工作原理

  1. 集群架构

    • Zookeeper通常部署为集群,称为Zookeeper Ensemble。
    • 集群中的每个服务器称为一个节点,节点分为领导者(Leader)和跟随者(Follower)。
    • Leader负责处理写请求,并同步到Followers,Followers负责处理读请求。
  2. 一致性协议

    • Zookeeper使用Zab协议(Zookeeper Atomic Broadcast)来保证集群的一致性。
    • Zab协议类似于Paxos协议,确保在Leader和Follower之间的状态同步和数据一致性。
  3. 数据复制

    • Zookeeper的每个节点都维护一个内存中的数据副本,通过事务日志和快照机制保证数据的持久性。
    • 当Leader接收到写请求时,它会生成事务ID(ZXID),并将请求广播给所有Followers进行复制。

Zookeeper的应用场景

  1. 配置管理

    • 集中式存储和管理配置信息,确保分布式系统中的各个组件使用一致的配置信息。
  2. 命名服务

    • 提供分布式命名服务,将资源名称映射到物理地址,实现动态服务发现。
  3. 分布式锁

    • 通过创建临时节点,实现分布式锁,确保在分布式环境中只有一个客户端可以访问共享资源。
  4. 集群管理

    • 管理集群中的节点状态,监控节点的加入和离开,实现高可用性和负载均衡。
  5. Leader选举

    • 在分布式系统中,通过Zookeeper实现Leader选举,确保系统中只有一个主节点进行操作。

在Spring Boot中集成Zookeeper

在Spring Boot中,可以使用Spring Cloud Zookeeper来简化与Zookeeper的集成。

  1. 添加依赖

    xml 复制代码
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper</artifactId>
    </dependency>
  2. 配置Zookeeper连接信息

    application.yml文件中配置Zookeeper服务器地址:

    yaml 复制代码
    spring:
      cloud:
        zookeeper:
          connect-string: localhost:2181
  3. 使用Zookeeper进行配置管理

    通过注解@EnableZookeeperConfig启用Zookeeper配置管理:

    java 复制代码
    @SpringBootApplication
    @EnableZookeeperConfig
    public class MyApplication {
        public static void main(String[] args) {
            SpringApplication.run(MyApplication.class, args);
        }
    }
  4. 使用Zookeeper进行服务注册和发现

    通过注解@EnableDiscoveryClient启用服务注册和发现:

    java 复制代码
    @SpringBootApplication
    @EnableDiscoveryClient
    public class MyApplication {
        public static void main(String[] args) {
            SpringApplication.run(MyApplication.class, args);
        }
    }

Zookeeper是一个强大的分布式协调服务,广泛应用于分布式系统的配置管理、命名服务、分布式锁、集群管理和Leader选举等场景。通过Spring Cloud Zookeeper,可以简化与Zookeeper的集成,实现分布式系统的高可用性和一致性。

使用Zookeeper实现分布式锁的方案

临时无序节点 + 重试(自旋)- 非公平锁

实现步骤

  1. 初始化锁目录

    • 确保锁目录存在,例如 /locks。如果不存在,则创建。
  2. 创建临时无序节点

    • 每个客户端尝试在 /locks 目录下创建一个临时无序节点,表示锁的请求。
    java 复制代码
    String lockPath = zk.create("/locks/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  3. 尝试获取锁

    • 获取 /locks 目录下所有子节点,检查当前是否只有自己一个节点存在。
    java 复制代码
    List<String> children = zk.getChildren("/locks", false);
    if (children.size() == 1 && children.contains(lockPath.substring("/locks/".length()))) {
        // 获取到锁
    } else {
        // 没有获取到锁,自旋重试
    }
  4. 自旋重试

    • 如果没有获取到锁,则进行自旋重试,直到获取到锁为止。
    java 复制代码
    while (true) {
        List<String> children = zk.getChildren("/locks", false);
        if (children.size() == 1 && children.contains(lockPath.substring("/locks/".length()))) {
            // 获取到锁
            break;
        } else {
            // 等待一段时间再重试
            Thread.sleep(100);
        }
    }
  5. 释放锁

    • 删除自己创建的临时节点,释放锁。
    java 复制代码
    zk.delete(lockPath, -1);

代码示例

以下是一个简单的分布式锁实现示例:

java 复制代码
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.List;

public class DistributedLock {
    private ZooKeeper zk;
    private String lockPath;
    private static final int SESSION_TIMEOUT = 30000;

    public DistributedLock(String zkHost) throws Exception {
        this.zk = new ZooKeeper(zkHost, SESSION_TIMEOUT, event -> {});
        Stat stat = zk.exists("/locks", false);
        if (stat == null) {
            zk.create("/locks", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    public void acquireLock() throws Exception {
        lockPath = zk.create("/locks/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        while (true) {
            List<String> children = zk.getChildren("/locks", false);
            if (children.size() == 1 && children.contains(lockPath.substring("/locks/".length()))) {
                System.out.println("Acquired lock: " + lockPath);
                return;
            } else {
                Thread.sleep(100);
            }
        }
    }

    public void releaseLock() throws Exception {
        zk.delete(lockPath, -1);
        System.out.println("Released lock: " + lockPath);
    }
}

优点

  • 实现简单:无需处理复杂的顺序和监听逻辑,代码简洁明了。
  • 适用于非公平锁场景:在一些应用场景中,锁的公平性并不是必须的,这种实现方法可能会更适用。

缺点

  • 不公平:锁的获取顺序不保证先来先得,可能会导致饥饿现象。
  • 性能问题:自旋重试机制在高并发场景下可能会增加Zookeeper的负载和网络流量。
  • 资源浪费:自旋重试会导致资源浪费,特别是在锁竞争激烈的情况下,频繁的重试会占用大量CPU和网络资源。

这种非公平锁的实现适合一些对锁公平性要求不高的应用场景,但在高并发和资源竞争激烈的场景下,可能需要考虑更复杂的实现方法,如临时顺序节点和监听器结合的方法。

临时顺序节点 + watch - 公平锁

实现步骤

  1. 初始化锁目录

    • 确保锁目录存在,例如 /locks。如果不存在,则创建。
  2. 创建临时顺序节点

    • 每个客户端尝试在 /locks 目录下创建一个临时顺序节点,表示锁的请求。
    java 复制代码
    String lockPath = zk.create("/locks/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
  3. 获取所有子节点并排序

    • 获取 /locks 目录下所有子节点,并按顺序排序。
    java 复制代码
    List<String> children = zk.getChildren("/locks", false);
    Collections.sort(children);
  4. 判断是否获取到锁

    • 如果当前节点是最小节点,则获取到锁。
    • 如果不是最小节点,则找到比当前节点小的前一个节点,并监听该节点的删除事件。
    java 复制代码
    String thisNode = lockPath.substring("/locks/".length());
    int index = children.indexOf(thisNode);
    if (index == 0) {
        // 获取到锁
    } else {
        String prevNode = children.get(index - 1);
        Stat stat = zk.exists("/locks/" + prevNode, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (event.getType() == Event.EventType.NodeDeleted) {
                    // 上一个节点被删除,尝试获取锁
                    acquireLock();
                }
            }
        });
        if (stat == null) {
            // 上一个节点已经不存在,重试获取锁
            acquireLock();
        }
    }
  5. 释放锁

    • 删除自己创建的临时顺序节点,释放锁。
    java 复制代码
    zk.delete(lockPath, -1);

代码示例

以下是一个简单的公平锁实现示例:

java 复制代码
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.Collections;
import java.util.List;

public class DistributedFairLock {
    private ZooKeeper zk;
    private String lockPath;
    private String thisNode;
    private static final int SESSION_TIMEOUT = 30000;

    public DistributedFairLock(String zkHost) throws Exception {
        this.zk = new ZooKeeper(zkHost, SESSION_TIMEOUT, event -> {});
        Stat stat = zk.exists("/locks", false);
        if (stat == null) {
            zk.create("/locks", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    public void acquireLock() throws Exception {
        this.lockPath = zk.create("/locks/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        this.thisNode = lockPath.substring("/locks/".length());

        while (true) {
            List<String> children = zk.getChildren("/locks", false);
            Collections.sort(children);
            int index = children.indexOf(thisNode);
            if (index == 0) {
                System.out.println("Acquired lock: " + lockPath);
                return;
            } else {
                String prevNode = children.get(index - 1);
                Stat stat = zk.exists("/locks/" + prevNode, new Watcher() {
                    @Override
                    public void process(WatchedEvent event) {
                        if (event.getType() == Event.EventType.NodeDeleted) {
                            try {
                                acquireLock();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
                if (stat == null) {
                    acquireLock();
                } else {
                    synchronized (this) {
                        wait();
                    }
                }
            }
        }
    }

    public void releaseLock() throws Exception {
        zk.delete(lockPath, -1);
        System.out.println("Released lock: " + lockPath);
    }
}

优点

  • 公平性:锁的获取顺序严格按照节点创建的顺序,保证公平性。
  • 效率高:只有在前一个节点释放锁时,才会尝试获取锁,减少不必要的重试。

缺点

  • 复杂度高:实现相对复杂,需要处理节点的监听和重试逻辑。
  • ZooKeeper负载:在高并发场景下,ZooKeeper的负载可能较高。

这种实现方式适合对锁的公平性要求较高的应用场景,如订单系统、队列系统等。通过临时顺序节点和Watch机制,确保了锁的获取顺序,有效避免了饥饿现象。

相关推荐
道一云黑板报3 小时前
Flink集群批作业实践:七析BI批作业执行
大数据·分布式·数据分析·flink·kubernetes
飞来又飞去5 小时前
kafka sasl和acl之间的关系
分布式·kafka
MZWeiei6 小时前
Zookeeper的监听机制
分布式·zookeeper
莹雨潇潇6 小时前
Hadoop完全分布式环境部署
大数据·hadoop·分布式
浩哲Zhe7 小时前
RabbitMQ
java·分布式·rabbitmq
明达技术7 小时前
分布式 IO 模块:赋能造纸业,革新高速纸机主传动
分布式
Allen Bright8 小时前
RabbitMQ中的Topic模式
分布式·rabbitmq
李洋-蛟龙腾飞公司9 小时前
HarmonyOS Next 应用元服务开发-分布式数据对象迁移数据权限与基础数据
分布式·华为·harmonyos
rainoway10 小时前
CRDT宝典 - Multi-Value-Register
前端·分布式·算法
MZWeiei11 小时前
Zookeeper的选举机制
大数据·分布式·zookeeper