1. 基础知识
1.1 角色
leader:处理所有的事务请求(写请求),可以处理读请求,集群中只能有一个Leader
Follower:只能处理读请求,同时作为 Leader的候选节点,即如果Leader宕机,Follower节点要参与到新的Leader选举中,有可能成为新的Leader节点。
Observer:只能处理读请求,不能参与选举。
1.2 与eureka的区别
-
zookeeper保证cp原则(一致性)而Eureka保证的是ap原则(可用性)
-
zookeeper在选举期间注册服务瘫痪不可用,而Eureka各个节点平等,只要一台就能保证服务可以,但查询到的数据不一定是最新的,可以很好的应对网络故障导致的部分节点失联
-
zookeeper有header和follower角色(当header挂掉,会从剩下的follower里面选举一个header),Eureka各个节点平等
-
zookeeper采用半数存活原则(避免脑裂),Eureka采用自我保护机制来解决分区问题
-
kafka就是使用的zookeeper作为注册中心,理论上Eureka更适合作为注册中心
1.3 应用场景
提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下 线、软负载均衡等。
(1)命令服务。可以简单理解为电话簿。打电话前,先查找这个人名对应的号码。分布式环境下,经常需要对应用/服务进行统一命名,便于识别不同的服务。类似于识别不同的服务。类似于域名与IP之间的对应关系,域名容易记住。Zookeeper通过名称来获取资源或服务的地址、提供者等信息。
(2)配置管理。分布式系统有大量的服务器,比如在搭建Hadoop的HDFS的时候,需要在一台Master主机器上配置好HDFS需要的各种配置文件,然后通过scp命令把这些配置文件复制到其他节点上,这样各个机器拿到的配置信息是一致的,才能成功运行HDFS服务。Zookeeper提供了这样一种服务:一种集中管理配置的方面,我们在这个集中的地方修改了配置,所有对这个配置感兴趣的服务都可以获取变更。这样就省去手动复制配置,还保证了可靠性和一致性。
(3)集群管理。集群管理包含两点:是否有机器退出和加入、选举Master。在分布式集群中,经常会由于各种原因,比如硬件故障、网络问题等,有些新的节点会加入进来,也有老的节点会退出集群。这个时候,集群中有些机器(比如Master节点)需要感知到这种变化,然后根据这种变化做出对应的决策。Zookeeper集群管理就是感知变化,做出对应的策略。
(4)分布式锁。Zookeeper的一致性文件系统使得锁的问题变得容易。锁服务可以分为两类,一类是保持独占;另一类是控制时序。单机程序的各个进程需要对互斥资源进行访问时需要加锁,分布式程序分布在各个主机上的进程对互斥资源进行访问时也需要加锁。
1.4 数据结构
ZooKeeper 提供的命名空间很像标准文件系统。名称是由斜杠 (/) 分隔的一系列路径元素。ZooKeeper 命名空间中的每个节点都由路径标识。
ZooKeeper 的分层命名空间:
节点和临时节点
与标准文件系统不同,ZooKeeper 命名空间中的每个节点都可以拥有与其关联的数据以及子节点。这就像拥有一个允许文件也成为目录的文件系统。(ZooKeeper 被设计用来存储协调数据:状态信息、配置、位置信息等,所以每个节点存储的数据通常很小,在字节到千字节的范围内。)我们使用术语znode来明确我们正在谈论 ZooKeeper 数据节点。
Znode 维护一个统计结构,其中包括数据更改、ACL 更改和时间戳的版本号,以允许缓存验证和协调更新。每次 znode 的数据更改时,版本号都会增加。例如,每当客户端检索数据时,它也会收到数据的版本。
存储在命名空间中每个 znode 的数据是原子读取和写入的。读取获取与 znode 关联的所有数据字节,写入替换所有数据。每个节点都有一个访问控制列表 (ACL),它限制谁可以做什么。
ZooKeeper 也有临时节点的概念。只要创建 znode 的会话处于活动状态,这些 znode 就存在。当会话结束时,znode 被删除。
1.5 节点
1.5.1 节点类型
- 临时节点:临时节点是一种特殊的节点,它只在创建它的客户端会话中存在。一旦客户端会话关闭,临时节点也将消失。临时节点的创建是有顺序的,如果多个客户端同时创建临时节点,则顺序节点的创建顺序是按照客户端发起请求的顺序进行的。
- 永久节点:永久节点是一种常规的节点,它会一直存在,直到被删除。永久节点的创建是有顺序的,如果多个客户端同时创建永久节点,则顺序节点的创建顺序是按照客户端发起请求的顺序进行的。
- 顺序节点:顺序节点是一种特殊的永久节点,它的创建顺序是按照客户端发起请求的顺序进行的。顺序节点的值是它的创建顺序,每个顺序节点都有一个唯一的值,即它的创建顺序。顺序节点的创建是有顺序的,如果多个客户端同时创建顺序节点,则顺序节点的创建顺序是按照客户端发起请求的顺序进行的。
使用场景:
- 临时节点:临时节点适用于需要在客户端会话结束时自动删除的场景,例如用于实现心跳机制、分布式锁等。
- 永久节点:永久节点适用于需要一直存在的场景,例如用于实现分布式系统中的配置信息、命名服务等。
- 顺序节点:顺序节点适用于需要按照特定顺序创建的场景,例如用于实现分布式系统中的数据同步、事务处理等。
2. docker安装
docker run -d --name zookeeper -p 2181:2181 zookeeper
3. 注册中心
3.1 基本概念
服务注册:
- springboot项目启动时,自定义监听器ApplicationListener去监听web服务启动事件
- web server启动成功,则触发事件回调方法
- 回调方法中,在zookeeper指定节点下创建临时节点,临时节点的值保存当前项目启动的 ip + port
- 如果某个服务宕机,服务断开一定时间(默认30s)临时节点会自动删除
服务发现:
- springboot项目启动时,会从zookeeper指定节点获取对应服务的所有可用url列表(可以缓存此url列表)
- 然后根据负载均衡算法,将请求负载到url列表中的某一个server上
- 利用spring初始化器扩展机制创建zookeeper节点监听,当节点列表发生变更,则更新url列表缓存
4. 整合 SpringBoot
4.1 父项目
目录结构
pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>zookeeper-demo-02</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zookeeper-demo-02</name>
<properties>
<java.version>1.8</java.version>
</properties>
<modules>
<module>provider-6601</module>
<module>consumer-8801</module>
</modules>
<packaging>pom</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.21</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
整合springboot需要引入的依赖(最关键的依赖):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<version>3.1.3</version>
</dependency>
4.2 provider-6601
pom文件(子项目引入都类似-除了个别包需要单独引入自己的依赖,依赖都直接继承于父项目的):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>zookeeper-demo-02</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>provider-6601</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>provider-6601</name>
<description>provider-6601</description>
<dependencies>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yaml:
server:
port: 6601
spring:
cloud:
discovery:
enabled: true #开启注册服务发现功能(默认开始)
zookeeper:
connect-string: 162.14.81.29:2181 #zookeeper注册中心地址
application:
name: test-zookeeper-provider-6601 #服务名
编写一个测试http接口:
@RestController
public class ProviderController {
@GetMapping(value = "/provider/zk")
public String provider() {
return "springcloud with zookeeper~~~~~";
}
}
4.3 consumer-8801
application.yaml
server:
port: 8801
spring:
cloud:
zookeeper:
connect-string: 162.14.81.29:2181
application:
name: test-zookeeper-consumer-8801
这里使用spring自带的RestTemplate去调用服务,注册RestTemplate。
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced //Spring Cloud中提供的一个注解,它用于将一个RestTemplate对象标记为支持负载均衡,针对服务名称进行REST调用
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
消费端测试controller
@RestController
public class ConsumerController {
@Resource
RestTemplate restTemplate;
//调用的域名就是 生产端的application的name
public static final String INVOKE_URL = "http://test-zookeeper-provider-6601";
@GetMapping(value = "/consumer/payment/zk")
public String paymentInfo() {
//请求的地址就是:域名+目标接口地址
return restTemplate.getForObject(INVOKE_URL + "/provider/zk", String.class);
}
}
5. 整合 doubbo
6. 客户端
6.1 curator
Curator是Netflix公司开源的一套Zookeeper客户端框架。了解过Zookeeper原生API都会清楚其复杂度。Curator帮助我们在其基础上进行封装、实现一些开发细节,包括接连重连、反复注册Watcher和NodeExistsException等。目前已经作为Apache的顶级项目出现,是最流行的Zookeeper客户端之一。从编码风格上来讲,它提供了基于Fluent的编程风格支持。
除此之外,Curator还提供了Zookeeper的各种应用场景:Recipe、共享锁服务、Master选举机制和分布式计数器等。
官网地址:https://curator.apache.org/getting-started.html
新版本变更:https://curator.apache.org/breaking-changes.html
6.1.1 项目组件
名称 | 描述 |
---|---|
Recipes | Zookeeper典型应用场景的实现,这些实现是基于Curator Framework。 |
Framework | Zookeeper API的高层封装,大大简化Zookeeper客户端编程,添加了例如Zookeeper连接管理、重试机制等。 |
Utilities | 为Zookeeper提供的各种实用程序。 |
Client | Zookeeper client的封装,用于取代原生的Zookeeper客户端(ZooKeeper类),提供一些非常有用的客户端特性。 |
Errors | Curator如何处理错误,连接问题,可恢复的例外等。 |
6.1.2 Maven依赖
Curator的jar包已经发布到Maven中心,由以下几个artifact的组成。根据需要选择引入具体的artifact。但大多数情况下只用引入curator-recipes即可。
GroupID/Org | ArtifactID/Name | 描述 |
---|---|---|
org.apache.curator | curator-recipes | 所有典型应用场景。需要依赖client和framework,需设置自动获取依赖。 |
org.apache.curator | curator-framework | 同组件中framework介绍。 |
org.apache.curator | curator-client | 同组件中client介绍。 |
org.apache.curator | curator-test | 包含TestingServer、TestingCluster和一些测试工具。 |
org.apache.curator | curator-examples | 各种使用Curator特性的案例。 |
org.apache.curator | curator-x-discovery | 在framework上构建的服务发现实现。 |
org.apache.curator | curator-x-discoveryserver | 可以喝Curator Discovery一起使用的RESTful服务器。 |
org.apache.curator | curator-x-rpc | Curator framework和recipes非java环境的桥接。 |
根据上面的描述,开发人员大多数情况下使用的都是curator-recipes的依赖,此依赖的maven配置如下:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>
6.1.3 创建回话
让spring容器接管这个bean,这样可以避免多次创建连接类。
@Configuration
public class CuratorConfig {
@Bean
public CuratorFramework getClient() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("162.14.81.29:2181", retryPolicy);
client.start();
return client;
}
}
6.1.4 常用功能
- create():创建一个ZooKeeper节点。
- createContainers():创建一个或多个ZooKeeper节点,同时创建父节点。
- delete():删除一个ZooKeeper节点。
- checkExists():检查一个ZooKeeper节点是否存在。
- setData():设置一个ZooKeeper节点的数据。
- getData():获取一个ZooKeeper节点的数据。
- getChildren():获取一个ZooKeeper节点的子节点。
- getACL():获取一个ZooKeeper节点的ACL权限列表。
- setACL():设置一个ZooKeeper节点的ACL权限列表。
- inTransaction():启动一个事务,用于执行多个操作。
- watched():监听一个ZooKeeper节点的变化。
- blockUntilConnected():阻塞直到与ZooKeeper服务器建立连接。
- start():启动CuratorFramework实例。
- close():关闭CuratorFramework实例。
基本代码示例:
@SpringBootTest
class Provider6601ApplicationTests {
@Resource
CuratorFramework curatorClient; //直接引入spring容器托管的bean
@Test
void contextLoads() throws Exception {
//创建一个节点
String string = curatorClient.create().forPath("/my");
System.out.println(string);
//获取节点列表
List<String> strings = curatorClient.getChildren().forPath("/");
//建立一个节点并储存数据
String string = curatorClient.create().forPath("/my/fun2", "asd".getBytes());
//设置数据
Stat stat = curatorClient.setData().forPath("/my/fun2", "123".getBytes());
}
}
6.2. zookeeper监听器
6.2.1 原理
ZooKeeper 的监听器原理如下:
- 客户端注册监听器:客户端调用注册监听器的方法(比如 exists、getData、getChildren、addWatch),传递节点路径和监听器,然后组装成一个 Packet 对象发送给服务端。
- 服务端存储监听器:服务端根据客户端的请求判断是否需要注册监听器,需要的话将节点路径和 ServerCnxn(表示客户端与服务端的连接)存储到服务端本地的 WatchManager 中,然后向客户端发送响应。
- 客户端接收服务端的响应:客户端接收到服务端的响应后,将监听器注册到客户端本地的 ZKWatcherManager。
- 服务端触发事件通知:对于客户端的 create、delete、setData 方法的调用会触发服务端向客户端发送事件通知。
- 客户端接收服务端的事件通知:客户端接收到服务端的事件通知后执行监听器的回调方法。
6.2.2 特点
exists 方法监听节点创建、删除、数据内容变化。
getData 方法监听节点删除、数据内容变化。
getChildren 方法监听节点的删除、子节点的创建或者删除。
这三个方法注册的监听器具有如下三个特点:
- 一次性 : (对于 Standard 类型的监听器,即默认类型)无论是服务端还是客户端,一旦监听器被触发,都会从存储中删除该监听器。因此需要反复注册,可以有效减轻服务器的压力。
- 客户端串行执行监听器的回调 : 因为是从阻塞队列中取出,保证了顺序性。
- 轻量 : 客户端向服务端注册监听器的时候,并不会把客户端的监听器对象传递给服务端,仅仅是在客户端请求中使用boolean类型的属性进行标记。服务端的监听器事件通知非常简单,只会通知客户端发生了事件,不会说明具体的事件内容。
从 3.6.0 版本开始,增加了 addWatch 方法,支持注册持久监听器(监听节点的创建、删除、数据内容变化)、持久递归监听器(监听节点/子节点的创建、删除、数据内容变化)。
6.2.3 基于curator实现
CuratorCache
节点事件包括创建、设值、删除等,对应的curator方法如下:
forCreates()
:创建forChanges()
:设值forCreatesAndChanges()
:创建和设值forDeletes()
:删除forAll()
:所有事件
默认情况下,listener监听整个子树(指定节点及其子节点)的事件,如下表所示:
forCreates() | forChanges() | forCreatesAndChanges() | forDeletes() | forAll() | |
---|---|---|---|---|---|
创建 | Y | N | Y | N | Y |
设值 | N | Y | Y | N | Y |
创建子节点 | Y | N | Y | N | Y |
为子节点设值 | N | Y | Y | N | Y |
创建子节点并为子节点设值 | N | Y | Y | N | Y |
删除子节点 | N | N | N | Y | Y |
删除节点 | N | N | N | Y | Y |
如果指定了 SINGLE_NODE_CACHE
选项,则只监听单个节点的事件,如下表所示:
forCreates() | forChanges() | forCreatesAndChanges() | forDeletes() | forAll() | |
---|---|---|---|---|---|
创建 | Y | N | Y | N | Y |
设值 | N | Y | Y | N | Y |
创建子节点 | N | N | N | N | N |
为子节点设值 | N | N | N | N | N |
创建子节点并为子节点设值 | N | N | N | N | N |
删除子节点 | N | N | N | N | N |
删除节点 | N | N | N | Y | Y |
注:如果删除多级节点,会触发多次。
@Test
void fun4() throws InterruptedException {
//使用的是默认选项
CuratorCache build = CuratorCache.build(curatorClient, "/my");
//注释行使用的是 SINGLE_NODE_CACHE 选项
CuratorCache build2 = CuratorCache.build(curatorClient, "/my", CuratorCache.Options.SINGLE_NODE_CACHE);
CuratorCacheListener listener = CuratorCacheListener.builder()
.forInitialized(() -> {
System.out.println("Initialized!");
})
.forCreates(childData -> System.out.println("创建! " + childData))
.forChanges((childData, childData1) -> System.out.println("变更! " + childData + ", " + childData1))
.forCreatesAndChanges((childData, childData1) -> System.out.println("创建+变更! " + childData + ", " + childData1))
.forDeletes(childData -> System.out.println("删除! " + childData))
.forAll((type, childData, childData1) -> System.out.println("All-所有操作! " + type + ", " + childData + ", " + childData1))
.build();
build.listenable().addListener(listener);
build.start();
while (true) {
Thread.sleep(1000);
}
}
创建节点:
修改节点:
删除节点:
6.3 分布式共享锁
该锁基于curator实现,需要引入的依赖如上一致。
什么是共享锁?
在分布式系统中,共享锁是一种同步机制,用于控制多个进程或线程对共享资源的访问。共享锁允许多个进程或线程同时读取共享资源,但只允许一个进程或线程进行写操作。这种机制确保了在写操作期间,其他进程或线程无法读取或写入共享资源,从而保证了数据的一致性和并发性。共享锁通常使用分布式锁的方式实现,例如使用分布式锁服务或基于分布式协议的锁机制。
什么的可重复?
可重入是指同一个线程可以多次获取同一个锁,而不会被阻塞。当线程已经持有锁时,再次请求获取该锁时,请求会立即成功,而不会被阻塞。可重入的概念也被称为递归锁。
什么是读写锁?
共享可重入读写锁是一种特殊类型的锁,它提供了读写分离的功能,允许多个线程同时读取共享资源,但只允许一个线程进行写操作。同时,同一个线程可以多次获取读锁,而不会被阻塞,这就是可重入的特性。
6.3.1 锁分类
锁名 | 锁名 | 说明 |
---|---|---|
共享可重入锁 | Shared Reentrant Lock | 全局同步的全分布式锁,这意味着在任何快照时间内没有两个客户端认为它们持有相同的锁。 |
共享锁 | Shared Lock | 类似于共享可重入锁但不可重入。 |
共享可重入读写锁 | Shared Reentrant Read Write Lock | 一种跨JVM工作的可重入读/写互斥锁。读写锁维护一对关联的锁,一个用于只读操作,一个用于写入。读锁可以由多个读取器进程同时持有,只要没有写入器。写锁是排他的。 |
共享信号量 | Shared Semaphore | 跨JVM工作的计数信号量。所有使用相同锁路径的JVM中的所有进程都将实现进程间有限的租约集。此外,这个信号量大多是“公平的”——每个用户都将按照请求的顺序获得租约(从ZK的角度来看)。 |
多共享锁 | Multi Shared Lock | 将多个锁作为一个实体进行管理的容器。当调用获取()时,将获取所有锁。如果失败,则释放所有获取的路径。类似地,当调用释放()时,将释放所有锁(失败被忽略)。 |
1. Shared Reentrant Lock
使用 InterProcessMutex
共享可重入锁有两种方式:
- acquire()
- acquire(long time, TimeUnit unit)
代码示例:
@Test
void fun5() {
Runnable runnable = () -> {
//定义自己的锁信息
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorClient, "/lock_test/a_001");
try {
interProcessMutex.acquire(); //所有会依次等待执行
Thread.sleep(10_000); //模拟业务处理
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
interProcessMutex.release();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
try {
Thread.sleep(60_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
代码示例2:
@Test
void fun5() {
Runnable runnable = () -> {
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorClient, "/lock_test/a_001");
try {
//加入等待时间,如果指定时间内为获取到锁返回为false,反之获取到锁为true
boolean acquire = interProcessMutex.acquire(5, TimeUnit.SECONDS);
if (!acquire) {
return; //没有获得锁的执行逻辑
}
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
Thread.sleep(10_000); //模拟业务处理
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
interProcessMutex.release();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
try {
Thread.sleep(60_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
2. Shared Lock
使用 InterProcessSemaphoreMutex
共享可重入锁有两种方式:
InterProcessSemaphoreMutex mutex = new InterProcessSemaphoreMutex(curatorClient, "/lock_02/fun6");
方法一:
代码示例:
@Test
void fun6() throws InterruptedException {
InterProcessSemaphoreMutex mutex = new InterProcessSemaphoreMutex(curatorClient, "/lock_02/fun6");
Runnable runnable = () -> {
try {
mutex.acquire();
// TODO 不支持可重入 == 会造成死锁
mutex.acquire();
Thread.sleep(5_000);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
mutex.release();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
Thread.sleep(60_000);
}
方法二:
3. 可重入读写锁
跨 JVM 工作的重入读/写互斥锁。 使用 Zookeeper 来保持锁。所有 JVM 中使用相同锁定路径的所有进程都将实现进程间关键部分。此外,这种互斥锁是“公平的”,每个用户将按照请求的顺序获得互斥锁(从ZK的角度来看)。
读写锁维护一对关联的锁,一个用于只读操作,一个用于写入。读锁定可以由多个读取器进程同时持有,只要没有写入器。写锁定是独占的。
重入此锁允许读取器和写入器以可重入锁的样式重新获取读取或写入锁。在释放写入线程/进程持有的所有写锁定之前,不允许使用非重入读取器。此外,写入器可以获取读锁定,但反之则不然。如果读取器尝试获取写锁定,则永远不会成功。
锁定降级重入还允许从写锁定降级到读锁定,方法是获取写锁定,然后获取读锁定,然后释放写锁定。但是,无法从读锁定升级到写锁定。
用法:
public InterProcessReadWriteLock(CuratorFramework client, String basePath) //创建进程间读写锁
一般用法:
public InterProcessLock readLock() //读锁
public InterProcessLock writeLock() //写锁
7. 配置中心
7.1 企业版
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Value("${my.property}")
private String myProperty;
private ZookeeperConfigWatcher configWatcher;
public MyCommandLineRunner(ZookeeperConfigWatcher configWatcher) {
this.configWatcher = configWatcher;
}
@Override
public void run(String... args) throws Exception {
// 获取远程配置文件的值
String remoteConfigValue = configWatcher.getConfigValue("my.property");
System.out.println("Remote Config Value: " + remoteConfigValue);
// 在这里可以进行配置的初始化操作
// 使用远程配置文件的值进行初始化
// ...
// 关闭Zookeeper连接
configWatcher.close();
}
}
public class Main {
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
String zookeeperAddress = "your-zookeeper-address:2181";
ZookeeperConfigWatcher configWatcher = new ZookeeperConfigWatcher(zookeeperAddress);
// 获取配置值
String configValue = configWatcher.getConfigValue("my.property");
System.out.println("Config Value: " + configValue);
// 监听配置变化
configWatcher.watchConfigChanges();
// 主线程等待,保持监听配置变化
Thread.sleep(Long.MAX_VALUE);
// 关闭连接
configWatcher.close();
}
}
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class ZookeeperConfigWatcher implements Watcher {
private static final String CONFIG_NODE_PATH = "/config";
private ZooKeeper zooKeeper;
private CountDownLatch connectedLatch;
public ZookeeperConfigWatcher(String zookeeperAddress) throws IOException, InterruptedException {
connectedLatch = new CountDownLatch(1);
zooKeeper = new ZooKeeper(zookeeperAddress, 5000, this);
connectedLatch.await();
}
public String getConfigValue(String key) throws KeeperException, InterruptedException {
String configNodePath = CONFIG_NODE_PATH + "/" + key;
byte[] data = zooKeeper.getData(configNodePath, this, null);
return new String(data);
}
public void watchConfigChanges() throws KeeperException, InterruptedException {
Stat stat = zooKeeper.exists(CONFIG_NODE_PATH, this);
if (stat != null) {
zooKeeper.getChildren(CONFIG_NODE_PATH, this);
}
}
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
if (event.getType() == Event.EventType.None && event.getPath() == null) {
connectedLatch.countDown();
} else if (event.getType() == Event.EventType.NodeChildrenChanged) {
try {
zooKeeper.getChildren(event.getPath(), this);
// Configuration has changed, handle the change
// 可以在这里进行配置变化的处理逻辑,比如重新加载配置
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void close() throws InterruptedException {
zooKeeper.close();
}
}