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机制,确保了锁的获取顺序,有效避免了饥饿现象。

相关推荐
KIDAKN5 小时前
RabbitMQ 初步认识
分布式·rabbitmq
pan3035074795 小时前
Kafka 和 RabbitMQ的选择
分布式·kafka·rabbitmq
hzulwy8 小时前
Kafka基础理论
分布式·kafka
明达智控技术9 小时前
MR30分布式IO在全自动中药煎药机中的应用
分布式·物联网·自动化
jakeswang10 小时前
细说分布式ID
分布式
失散1311 小时前
分布式专题——1.2 Redis7核心数据结构
java·数据结构·redis·分布式·架构
王中阳Go12 小时前
头一次见问这么多kafka的问题
分布式·kafka
boonya13 小时前
Kafka核心原理与常见面试问题解析
分布式·面试·kafka
KIDAKN14 小时前
RabbitMQ 重试机制 和 TTL
分布式·rabbitmq
JAVA学习通15 小时前
【RabbitMQ】----初识 RabbitMQ
分布式·rabbitmq