Zookeeper是一个分布式协调服务,用于管理和协调分布式应用程序的组件。它提供了集中式的服务,用于维护配置信息、命名、分布式同步和组服务。Zookeeper可以帮助开发人员简化分布式应用的设计和实现。
Zookeeper的核心概念
-
节点(ZNode):
- Zookeeper的数据模型类似于文件系统,由一棵层次化的节点树组成。
- 每个节点称为ZNode,可以存储数据和子节点。
- ZNode有两种类型:临时节点(Ephemeral)和持久节点(Persistent)。
- 临时节点:会话结束时自动删除。
- 持久节点:需要明确删除操作才能删除。
-
会话(Session):
- 客户端与Zookeeper服务器之间的连接称为会话。
- 会话是有超时时间的,客户端需要定期发送心跳来维持会话。
-
版本(Version):
- 每个ZNode都有版本信息,包括数据版本(dataVersion)、子节点版本(cversion)和ACL版本(aversion)。
- 版本信息在更新时自动递增,用于实现乐观锁机制。
-
监视(Watchers):
- 客户端可以在ZNode上设置监视器,当ZNode发生变化时,客户端会收到通知。
- 监视是一次性的,需要重新设置。
Zookeeper的工作原理
-
集群架构:
- Zookeeper通常部署为集群,称为Zookeeper Ensemble。
- 集群中的每个服务器称为一个节点,节点分为领导者(Leader)和跟随者(Follower)。
- Leader负责处理写请求,并同步到Followers,Followers负责处理读请求。
-
一致性协议:
- Zookeeper使用Zab协议(Zookeeper Atomic Broadcast)来保证集群的一致性。
- Zab协议类似于Paxos协议,确保在Leader和Follower之间的状态同步和数据一致性。
-
数据复制:
- Zookeeper的每个节点都维护一个内存中的数据副本,通过事务日志和快照机制保证数据的持久性。
- 当Leader接收到写请求时,它会生成事务ID(ZXID),并将请求广播给所有Followers进行复制。
Zookeeper的应用场景
-
配置管理:
- 集中式存储和管理配置信息,确保分布式系统中的各个组件使用一致的配置信息。
-
命名服务:
- 提供分布式命名服务,将资源名称映射到物理地址,实现动态服务发现。
-
分布式锁:
- 通过创建临时节点,实现分布式锁,确保在分布式环境中只有一个客户端可以访问共享资源。
-
集群管理:
- 管理集群中的节点状态,监控节点的加入和离开,实现高可用性和负载均衡。
-
Leader选举:
- 在分布式系统中,通过Zookeeper实现Leader选举,确保系统中只有一个主节点进行操作。
在Spring Boot中集成Zookeeper
在Spring Boot中,可以使用Spring Cloud Zookeeper来简化与Zookeeper的集成。
-
添加依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-zookeeper</artifactId> </dependency>
-
配置Zookeeper连接信息:
在application.yml
文件中配置Zookeeper服务器地址:spring:cloud:zookeeper:connect-string: localhost:2181
-
使用Zookeeper进行配置管理:
通过注解@EnableZookeeperConfig
启用Zookeeper配置管理:@SpringBootApplication @EnableZookeeperConfig public class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class, args);} }
-
使用Zookeeper进行服务注册和发现:
通过注解@EnableDiscoveryClient
启用服务注册和发现:@SpringBootApplication @EnableDiscoveryClient public class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class, args);} }
Zookeeper是一个强大的分布式协调服务,广泛应用于分布式系统的配置管理、命名服务、分布式锁、集群管理和Leader选举等场景。通过Spring Cloud Zookeeper,可以简化与Zookeeper的集成,实现分布式系统的高可用性和一致性。
使用Zookeeper实现分布式锁的方案
临时无序节点 + 重试(自旋)- 非公平锁
实现步骤
-
初始化锁目录
- 确保锁目录存在,例如
/locks
。如果不存在,则创建。
- 确保锁目录存在,例如
-
创建临时无序节点
- 每个客户端尝试在
/locks
目录下创建一个临时无序节点,表示锁的请求。
String lockPath = zk.create("/locks/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
- 每个客户端尝试在
-
尝试获取锁
- 获取
/locks
目录下所有子节点,检查当前是否只有自己一个节点存在。
List<String> children = zk.getChildren("/locks", false); if (children.size() == 1 && children.contains(lockPath.substring("/locks/".length()))) {// 获取到锁 } else {// 没有获取到锁,自旋重试 }
- 获取
-
自旋重试
- 如果没有获取到锁,则进行自旋重试,直到获取到锁为止。
while (true) {List<String> children = zk.getChildren("/locks", false);if (children.size() == 1 && children.contains(lockPath.substring("/locks/".length()))) {// 获取到锁break;} else {// 等待一段时间再重试Thread.sleep(100);} }
-
释放锁
- 删除自己创建的临时节点,释放锁。
zk.delete(lockPath, -1);
代码示例
以下是一个简单的分布式锁实现示例:
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 - 公平锁
实现步骤
-
初始化锁目录
- 确保锁目录存在,例如
/locks
。如果不存在,则创建。
- 确保锁目录存在,例如
-
创建临时顺序节点
- 每个客户端尝试在
/locks
目录下创建一个临时顺序节点,表示锁的请求。
String lockPath = zk.create("/locks/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
- 每个客户端尝试在
-
获取所有子节点并排序
- 获取
/locks
目录下所有子节点,并按顺序排序。
List<String> children = zk.getChildren("/locks", false); Collections.sort(children);
- 获取
-
判断是否获取到锁
- 如果当前节点是最小节点,则获取到锁。
- 如果不是最小节点,则找到比当前节点小的前一个节点,并监听该节点的删除事件。
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() {@Overridepublic void process(WatchedEvent event) {if (event.getType() == Event.EventType.NodeDeleted) {// 上一个节点被删除,尝试获取锁acquireLock();}}});if (stat == null) {// 上一个节点已经不存在,重试获取锁acquireLock();} }
-
释放锁
- 删除自己创建的临时顺序节点,释放锁。
zk.delete(lockPath, -1);
代码示例
以下是一个简单的公平锁实现示例:
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() {@Overridepublic 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机制,确保了锁的获取顺序,有效避免了饥饿现象。