Spring Cloud与Zookeeper:打造高效分布式协调系统
Spring Cloud 与 Zookeeper:打造高效分布式协调系统
在微服务架构下,如何保证各个服务实例之间的协调、配置和通信是一项核心难题。ZooKeeper 作为一个高性能的分布式协调框架,为服务注册与发现、分布式配置、分布式锁等场景提供了统一的解决方案。而 Spring Cloud Zookeeper 则将 ZooKeeper 与 Spring 生态无缝对接,让我们可以在熟悉的 Spring Boot 项目中,快速构建稳定、可扩展的分布式系统。
本文将从以下几个方面,结合代码示例与图解,详细讲解如何使用 Spring Cloud 与 ZooKeeper 打造高效的分布式协调系统:
- 环境准备
- ZooKeeper 基础与 Spring Cloud Zookeeper 概览
- 3.1. 依赖与配置
- 3.2. 服务提供者示例
- 3.3. 服务消费者示例
- 3.4. 注册发现流程图解
- 4.1. ZooKeeper 上存放配置
- 4.2. Spring Cloud Zookeeper Config 配置与代码
- 4.3. 配置拉取与刷新流程图解
- 5.1. Curator 基础与依赖
- 5.2. 实现分布式锁的代码示例
- 5.3. 分布式锁使用流程图解
- 监控与运维要点
- 总结
环境准备
在动手之前,我们需要准备以下环境:
- JDK 1.8+
- Maven 3.5+
- ZooKeeper 3.5.x 或 3.6.x
- Spring Boot 2.3.x 或更高
- Spring Cloud Hoxton.RELEASE / Spring Cloud 2020.x(本文示例基于 Spring Cloud Hoxton)
- 开发工具:IntelliJ IDEA / Eclipse 等
1. 启动 ZooKeeper
本地开发中,可以通过 Docker 方式快速启动一个单节点 ZooKeeper 实例:
# 拉取官方镜像并运行
docker run -d --name zk -p 2181:2181 zookeeper:3.6.2
# 检查是否正常启动
docker logs zk
# 看到 "binding to port 0.0.0.0/0.0.0.0:2181" 便代表 zk 已正常启动
如果不使用 Docker,也可自行从官网(https://zookeeper.apache.org/)下载并解压,编辑 conf/zoo.cfg
,然后:
# 进入解压目录
bin/zkServer.sh start
# 检查状态
bin/zkServer.sh status
默认情况下,ZooKeeper 会监听 localhost:2181
。
ZooKeeper 基础与 Spring Cloud Zookeeper 概览
2.1 ZooKeeper 核心概念
ZNode
ZooKeeper 数据模型类似于一棵树(称为znodes 树),每个节点(称为 ZNode)都可以存储少量数据,并可拥有子节点。ZNode 有两种主要类型:- 持久节点(Persistent ZNode):客户端创建后,除非显式删除,否则不会过期。
- 临时节点(Ephemeral ZNode):由客户端会话(Session)控制,一旦与 ZooKeeper 的连接断开,该节点会自动删除。
- Watch 机制
客户端可在 ZNode 上注册 Watch,当节点数据变化(如创建、删除、数据更新)时,ZooKeeper 会触发 Watch 通知客户端,便于实现分布式事件通知。 - 顺序节点(Sequential)
ZooKeeper 支持给节点名称追加自增序号,保证在同一个父节点下,子节点具有严格的顺序编号。这在 leader 选举、队列实现等场景非常常用。
2.2 Spring Cloud Zookeeper 概览
Spring Cloud 为我们提供了两个与 ZooKeeper 紧密集成的模块:
spring-cloud-starter-zookeeper-discovery
- 用于服务注册与发现。底层会在 ZooKeeper 上创建临时顺序节点(Ephemeral Sequential ZNode),注册服务信息,并定期心跳。其他消费者可通过 ZooKeeper 的 Watch 机制,实时获取注册列表。
spring-cloud-starter-zookeeper-config
- 用于分布式配置中心。将配置信息存储在 ZooKeeper 的某个路径下,Spring Cloud 在启动时会从 ZooKeeper 拉取配置并加载到 Spring 环境中,支持动态刷新(与 Spring Cloud Bus 联动)。
了解了这两个模块的作用后,我们可以根据不同场景,灵活使用 Spring Cloud Zookeeper 来完成分布式协调相关功能。
服务注册与发现示例
分布式系统下,服务实例可能动态上下线。传统的硬编码地址方式无法满足弹性扩缩容需求。通过 ZooKeeper 作为注册中心,每个服务启动时将自身元信息注册到 ZooKeeper,消费者动态从注册中心获取可用实例列表并发起调用,极大简化了运维复杂度。
3.1 依赖与全局配置
假设我们使用 Spring Cloud Hoxton.RELEASE 版本,并在 pom.xml
中引入以下依赖:
<!-- spring-boot-starter-parent 版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.RELEASE</version>
<relativePath/>
</parent>
<properties>
<!-- Spring Cloud 版本 -->
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud Zookeeper Discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!-- 如需读取配置信息,也可同时引入 Config Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-config</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- 引入 Spring Cloud BOM -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
所有微服务都需要配置与 ZooKeeper 的连接信息。在 application.yml
(或 application.properties
)中添加以下全局配置:
spring:
application:
name: ${SERVICE_NAME:demo-service} # 服务名称,可通过环境变量覆盖
cloud:
zookeeper:
connect-string: 127.0.0.1:2181 # ZooKeeper 地址
discovery:
enabled: true # 启用服务注册与发现
# 如需配置路径前缀,可通过 base-path 设置
# base-path: /services
说明:
spring.cloud.zookeeper.connect-string
:指定 ZooKeeper 的 IP\:Port,可填写集群(逗号分隔)。spring.cloud.zookeeper.discovery.enabled
:开启 Zookeeper 作为服务注册中心。spring.application.name
:服务注册到 ZooKeeper 时所使用的节点名称(ZNode 名称)。
接下来,我们基于上述依赖和全局配置,实现一个简单的服务提供者和消费者示例。
3.2 服务提供者示例
1. Main 类与注解
在服务提供者项目下创建主类,添加 @EnableDiscoveryClient
注解,启用服务注册:
package com.example.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 启用服务注册功能
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
2. Controller 暴露简单接口
创建一个 REST 控制器,提供一个返回“Hello from provider”的示例接口,并带上服务端口以示区分:
package com.example.provider.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Value("${server.port}")
private String port;
@GetMapping("/hello")
public String hello() {
return "Hello from provider, port: " + port;
}
}
3. application.yml
配置
在 src/main/resources/application.yml
中添加以下内容:
server:
port: 8081
spring:
application:
name: provider-service
cloud:
zookeeper:
connect-string: 127.0.0.1:2181
discovery:
enabled: true
# 可选:可自己定义注册时所处路径
# root-node: /services
启动后,当服务初始化完成并与 ZooKeeper 建立会话时,Spring Cloud Zookeeper 会在路径 /provider-service
(或结合 root-node
定制的路径)下创建一个临时顺序节点(Ephemeral Sequential)。该节点中会包含该实例的元数据(如 IP、端口、权重等)。
Node 结构示意(ZooKeeper):
/provider-service ├─ instance_0000000001 (data: {"instanceId":"10.0.0.5:8081","port":8081,…}) ├─ instance_0000000002 (data: {...}) └─ ……
- 由于是临时节点,服务实例下线或心跳中断,节点会自动删除,实现自动剔除失效实例。
3.3 服务消费者示例
1. Main 类与注解
在服务消费者项目下,同样添加 @EnableDiscoveryClient
:
package com.example.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 启用服务发现
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
2. RestTemplate Bean 注册
为了方便发起 HTTP 请求,我们使用 RestTemplate
并结合 @LoadBalanced
注解,让其支持通过服务名发起调用:
package com.example.consumer.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 使 RestTemplate 支持 Ribbon(或 Spring Cloud Commons)的负载均衡,自动从注册中心获取实例列表
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
说明:
@LoadBalanced
标注的RestTemplate
会自动拦截http://service-name/…
形式的调用,并将service-name
替换为可用实例列表(由 ZooKeeper 提供)。- 在 Spring Cloud Hoxton 及以上版本中,不再强制使用 Ribbon,调用流程由 Spring Cloud Commons 的负载均衡客户端负责。
3. 构建调用接口
新建一个控制器,通过注入 DiscoveryClient
查询所有 provider-service
的实例列表,并使用 RestTemplate
发起调用:
package com.example.consumer.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* 演示服务发现与调用
*/
@RestController
public class ConsumerController {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
@GetMapping("/invoke-provider")
public String invokeProvider() {
// 1. 从注册中心(ZooKeeper)获取 provider-service 的所有实例
List<ServiceInstance> instances = discoveryClient.getInstances("provider-service");
if (instances == null || instances.isEmpty()) {
return "No available instances";
}
// 简单起见,这里只拿第一个实例的 URI
String url = instances.get(0).getUri().toString() + "/hello";
// 2. 通过 RestTemplate 发起调用
return restTemplate.getForObject(url, String.class);
}
@GetMapping("/invoke-via-loadbalance")
public String invokeViaLoadBalance() {
// 通过 LoadBalanced RestTemplate,直接以服务名发起调用
String url = "http://provider-service/hello";
return restTemplate.getForObject(url, String.class);
}
}
4. application.yml
配置
server:
port: 8082
spring:
application:
name: consumer-service
cloud:
zookeeper:
connect-string: 127.0.0.1:2181
discovery:
enabled: true
启动消费者后,可以通过访问 http://localhost:8082/invoke-provider
或 http://localhost:8082/invoke-via-loadbalance
来间接调用 provider-service
,并实时感知集群实例变更。
3.4 注册发现流程图解
下面用一张简化的 ASCII 图,展示从服务提供者注册,到消费者发现并调用的大致流程:
┌──────────────────────────────────────────────────────────────┐
│ ZooKeeper │
│ (127.0.0.1:2181 单节点示例) │
│ │
│ /provider-service │
│ ├─ instance_0000000001 <- 临时顺序节点,data 包含服务IP:8081 │
│ └─ instance_0000000002 <- 另一台机器上的 provider 实例 │
│ │
│ /consumer-service │
│ └─ instance_0000000001 <- 消费者自身也会注册到 ZooKeeper │
│ │
└──────────────────────────────────────────────────────────────┘
▲ ▲
│ │
│ 1. ProviderApplication 启动 │ 4. ConsumerApplication 启动
│ - 创建 /provider-service/instance_0000000001 临时节点 │
│ │ - 创建 /consumer-service/instance_0000000001
│ │
┌────────────────┐ ┌────────────────┐
│ Provider (8081) │ │ Consumer (8082) │
│ @EnableDiscoveryClient │ @EnableDiscoveryClient
│ │
│ 2. Spring Cloud ZK Client 与 ZooKeeper 建立会话 │
│ - 注册元数据 (IP、端口、权重等) │
└────────────────┘ └────────────────┘
│ │
│ 3. ConsumerController 调用 │
│ discoveryClient.getInstances("provider-service") │
│ ZooKeeper 返回实例列表实例 │
│ │
│ ServiceInstance 列表: [ │
│ {instanceId=instance_0000000001, URI=http://10.0.0.5:8081}, │
│ {…第二个实例…} ] │
│ │
│ 5. RestTemplate 通过实例 IP:8081 发起 HTTP 请求 │
│ │
▼ ▼
┌────────────────────┐ ┌─────────────────────┐
│ “Hello from provider, port:8081” │ │ Consumer 返回给客户端 │
└────────────────────┘ └─────────────────────┘
- 1. 提供者启动后,Spring Cloud Zookeeper 自动在 ZooKeeper 上创建
/provider-service/instance_xxx
的临时顺序节点。 - 2. 该临时节点包含元数据信息,可在 ZooKeeper 客户端(如 zkCli、ZooInspector)中查看。
- 3. 消费者启动后,从
/provider-service
下获取所有子节点列表,即可得知哪些 provider 实例正在运行。 - 4. 消费者通过 RestTemplate 或者手动拼装 URL,发送 HTTP 请求实现跨实例调用。
这种基于 ZooKeeper 的注册与发现机制,天然支持实例下线(临时节点自动删除)、节点故障感知(Watch 通知)等分布式协调特性。
分布式配置示例
除了服务注册与发现,ZooKeeper 常被用于存储分布式配置,使多环境、多实例能够在运行时动态拉取配置信息。Spring Cloud Zookeeper Config 模块将 ZooKeeper 路径中的配置,作为 Spring Boot 的配置源注入。
4.1 ZooKeeper 上存放配置
创建 ZooKeeper 上的配置节点树
假设我们要为provider-service
存放配置信息,可在 ZooKeeper 根路径下建立如下结构:/config └─ provider-service ├─ application.yml (全局配置) └─ dev └─ application.yml (dev 环境特定配置)
往
/config/provider-service/application.yml
中放入内容
例如:# /config/provider-service/application.yml 中的数据(以 zkCli 或其他方式写入) message: prefix: "Hello from ZooKeeper Config"
- 如果有多环境需求,如 dev、prod,可创建
/config/provider-service/dev/application.yml
、/config/provider-service/prod/application.yml
来覆盖对应环境的属性。
写入示例(使用 zkCli)
# 进入 zkCli ./zkCli.sh -server 127.0.0.1:2181 # 创建 /config 节点(持久节点) create /config "" # 创建 provider-service 节点 create /config/provider-service "" # 在 /config/provider-service 下创建 application.yml,并写入配置 create /config/provider-service/application.yml "message:\n prefix: \"Hello from ZooKeeper Config\"" # 如需覆盖 dev 环境,可: create /config/provider-service/dev "" create /config/provider-service/dev/application.yml "message:\n prefix: \"[DEV] Hello from ZooKeeper Config\""
4.2 Spring Cloud Zookeeper Config 配置与代码
要让 Spring Boot 应用从 ZooKeeper 拉取配置,需要在 bootstrap.yml
(注意:必须是 bootstrap.yml
而非 application.yml
,因为 Config 在应用上下文初始化时就要加载)中进行如下配置:
# src/main/resources/bootstrap.yml
spring:
application:
name: provider-service # 与 ZooKeeper 中 /config/provider-service 对应
cloud:
zookeeper:
connect-string: 127.0.0.1:2181
config:
enabled: true # 开启 ZK Config
root: /config # 配置在 ZooKeeper 中的根路径
default-context: application # 加载 /config/provider-service/application.yml
# profile-separator: "/" # 默认 "/" 即 /config/{service}/{profile}/{context}.yml
解释:
spring.cloud.zookeeper.config.root
:指定 ZooKeeper 上存放配置的根路径(对应 zkCli 中创建的/config
)。spring.application.name
:用于定位子路径/config/provider-service
,从而加载该目录下的application.yml
。- 如果设置了
spring.profiles.active=dev
,则同时会加载/config/provider-service/dev/application.yml
并覆盖同名属性。
1. Main 类与注解
package com.example.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.context.config.annotation.RefreshScope;
@SpringBootApplication
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
2. 使用 ZK 配置的 Bean
借助 @RefreshScope
,我们可以实现配置的动态刷新。以下示例展示了如何将 ZooKeeper 中的 message.prefix
属性注入到业务代码中:
package com.example.provider.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RefreshScope // 支持动态刷新
@RestController
public class ConfigController {
@Value("${message.prefix}")
private String prefix;
@GetMapping("/zk-config-message")
public String getZkConfigMessage() {
return prefix + ", port: " + System.getenv("SERVER_PORT");
}
}
此时,只要我们在 ZooKeeper 上更新 /config/provider-service/application.yml
中的 message.prefix
值,且在应用运行时触发一次刷新(如调用 /actuator/refresh
,需引入 Spring Boot Actuator),即可让 @Value
注入的属性生效更新。
3. application.yml
(与 bootstrap.yml
区分开)
bootstrap.yml
用于配置 Spring Cloud Config Client 相关属性(优先级更高)。application.yml
用于常规应用级配置,比如服务器端口、日志配置等。
在 application.yml
中只需配置常规内容即可,例如:
# src/main/resources/application.yml
server:
port: ${SERVER_PORT:8081}
logging:
level:
root: INFO
4.3 配置拉取与刷新流程图解
┌──────────────────────────────────────────────────────────────────┐
│ ZooKeeper │
│ (127.0.0.1:2181 单节点示例) │
│ │
│ /config │
│ └─ provider-service │
│ ├─ application.yml (message.prefix = "Hello from ZK") │
│ └─ dev │
│ └─ application.yml (message.prefix = "[DEV] Hello") │
│ │
└──────────────────────────────────────────────────────────────────┘
▲ ▲
│ 1. Provider 启动时读取 bootstrap.yml 中 │
│ 的 ZK Config 配置 │
│ │
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ ProviderApplication │ │ ZooKeeper Config Path Tree │
│ Spring Boot 初始化时: │ │ root: /config │
│ - 查找 /config/provider-service/application.yml │
│ - 读取 message.prefix="Hello from ZK" │
└───────────────────────────────┘ └───────────────────────────────┘
│ 2. 将 ZK 中的属性注入到 Spring Environment │
▼
┌───────────────────────────────────────────────────────────────────┐
│ Spring Boot 应用上下文 │
│ - 启动完成后,ConfigController 中的 prefix="Hello from ZK" │
│ - 可通过 /zk-config-message 接口读取到最新值 │
└───────────────────────────────────────────────────────────────────┘
│
│ 3. 若在 zkCli 中执行:
│ set /config/provider-service/application.yml
│ "message.prefix: 'Updated from ZK'"
│
│ 4. 在应用运行时调用 /actuator/refresh (需启用 Actuator)
│ Spring Cloud 会重新拉取 ZK 上的配置,并刷新 @RefreshScope Bean
▼
┌───────────────────────────────────────────────────────────────────┐
│ Spring Environment 动态刷新 │
│ - prefix 属性更新为 "Updated from ZK" │
│ - 访问 /zk-config-message 即可获取最新值 │
└───────────────────────────────────────────────────────────────────┘
分布式锁示例
在分布式场景中,往往需要多实例对共享资源进行互斥访问。例如并发限流、分布式队列消费、分布式任务调度等场景,分布式锁是基础保障。ZooKeeper 原生提供了顺序临时节点等机制,Apache Curator(Netflix 出品的 ZooKeeper 客户端封装库)则进一步简化了分布式锁的使用。Spring Cloud Zookeeper 本身不直接提供锁相关 API,但我们可以在 Spring Boot 应用中引入 Curator,再结合 ZooKeeper 实现分布式锁。
5.1 Curator 基础与依赖
1. 添加 Maven 依赖
在项目的 pom.xml
中添加以下 Curator 相关依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.1</version>
</dependency>
- curator-framework:Curator 的基础 API,用于创建 ZooKeeper 客户端连接。
- curator-recipes:Curator 提供的各种“食谱”(Recipes),如分布式锁、Barrier、Leader 选举等。这里我们重点使用分布式锁(InterProcessMutex)。
2. 配置 CuratorFramework Bean
在 Spring Boot 中创建一个配置类,用于初始化 CuratorFramework
并注入到 Spring 容器中:
package com.example.lock.config;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ZkCuratorConfig {
@Bean(initMethod = "start", destroyMethod = "close")
public CuratorFramework curatorFramework() {
// ExponentialBackoffRetry 参数:初始重试时间、最大重试次数、最大重试时间
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
return CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.connectionTimeoutMs(3000)
.retryPolicy(retryPolicy)
.build();
}
}
connectString
:指定 ZooKeeper 地址,可填集群地址列表sessionTimeoutMs
:会话超时时间retryPolicy
:重试策略,这里使用指数退避重试
CuratorFramework Bean 会在容器启动时自动调用 start()
,在容器关闭时调用 close()
,完成与 ZooKeeper 的连接和资源释放。
5.2 实现分布式锁的代码示例
1. 分布式锁工具类
以下示例封装了一个简单的分布式锁工具,基于 Curator 的 InterProcessMutex
:
package com.example.lock.service;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class DistributedLockService {
private static final String LOCK_ROOT_PATH = "/distributed-lock";
@Autowired
private CuratorFramework curatorFramework;
/**
* 获取分布式锁
*
* @param lockName 锁名称,在 ZooKeeper 下会对应 /distributed-lock/{lockName} 路径
* @param timeoutSec 获取锁超时时间(秒)
* @return InterProcessMutex 对象,若获取失败返回 null
*/
public InterProcessMutex acquireLock(String lockName, long timeoutSec) throws Exception {
String lockPath = LOCK_ROOT_PATH + "/" + lockName;
// 创建 InterProcessMutex,内部会在 lockPath 下创建临时顺序节点
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
// 尝试获取锁,超时后无法获取则返回 false
boolean acquired = lock.acquire(timeoutSec, TimeUnit.SECONDS);
if (acquired) {
return lock;
} else {
return null;
}
}
/**
* 释放分布式锁
*
* @param lock InterProcessMutex 对象
*/
public void releaseLock(InterProcessMutex lock) {
if (lock != null) {
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
- 构造
InterProcessMutex(curatorFramework, lockPath)
时,Curator 会在/distributed-lock/lockName
路径下创建临时顺序子节点,形成分布式锁队列。 lock.acquire(timeout, unit)
:尝试获取锁,阻塞直到成功或超时。lock.release()
:释放锁时,Curator 会删除自己创建的临时节点,并通知后续等待的客户端。
2. Controller 使用示例
新建一个 REST 控制器,模拟多实例并发争抢锁的场景:
package com.example.lock.controller;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.lock.service.DistributedLockService;
@RestController
public class LockController {
@Autowired
private DistributedLockService lockService;
@GetMapping("/execute-with-lock")
public String executeWithLock() {
String lockName = "my-lock";
InterProcessMutex lock = null;
try {
// 尝试获取锁,超时时间 5 秒
lock = lockService.acquireLock(lockName, 5);
if (lock == null) {
return "无法获取分布式锁,请稍后重试";
}
// 模拟业务执行
Thread.sleep(2000);
return "执行成功,当前线程获得锁并完成业务逻辑";
} catch (Exception e) {
return "执行异常:" + e.getMessage();
} finally {
// 释放锁
lockService.releaseLock(lock);
}
}
}
启动多个服务实例(端口不同),同时访问 http://localhost:{port}/execute-with-lock
,只有第一个获取到锁的实例会真正执行业务,其他请求要么阻塞等待,要么在超时后返回“无法获取锁”。
5.3 分布式锁使用流程图解
┌───────────────────────────────────────────────────────────────────┐
│ ZooKeeper │
│ (127.0.0.1:2181) │
│ │
│ /distributed-lock │
│ ├─ my-lock/LOCK-0000000001 (临时顺序节点) │
│ ├─ my-lock/LOCK-0000000002 │
│ └─ … │
│ │
└───────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ 1. 实例A 调用 acquireLock("my-lock") │
│ → 在 /distributed-lock/my-lock 下创建 │
│ 临时顺序节点 LOCK-0000000001 (最小序号) │
│ → 获取到锁 │
┌───────────────────┐ 2. 实例B 同时调用 acquireLock("my-lock") ┌───────────────────┐
│ 实例A (port:8081) │─────▶ 在 /distributed-lock/my-lock 下创建 │ 实例B (port:8082) │
│ acquire() → LOCK-0000000001 (最小) │ 临时顺序节点 LOCK-0000000002 (次小) │
│ 成功获得锁 │◀───────── │ 等待 LOCK-0000000001 释放锁 │
└───────────────────┘ 3. 实例A 释放锁 (release()) └───────────────────┘
│ ▲ │
│ 4. ZooKeeper 删除 LOCK-0000000001 → 触发 B 的 Watch │
│ │ │
▼ │ ▼
┌───────────────────────────┐ │ 5. 实例B 发现自己序号最小,获得锁 ┌───────────────────────────┐
│ 实例A 完成业务逻辑;退出 │ │ (执行 acquire() 返回成功) │ 实例B 完成业务逻辑 │
└───────────────────────────┘ │ └───────────────────────────┘
│
│ 6. 依此类推,其他实例继续排队获取锁
通过 Curator 封装的 InterProcessMutex
,我们不需要手动实现序号节点的创建、Watch 监听等底层逻辑,只需调用 acquire()
与 release()
即可保障互斥访问。
监控与运维要点
ZooKeeper 集群化
- 生产环境建议至少搭建 3\~5 节点的 ZooKeeper 集群,保证分布式协调的可靠性与可用性。
- 使用投票机制(过半数)进行 leader 选举,避免出现脑裂。
ZooKeeper 数据结构管理
为不同功能(服务注册、配置、锁、队列等)合理规划 ZNode 路径前缀,例如:
/services/{service-name}/instance-00001 /config/{application}/{profile}/… /distributed-lock/{lock-name}/… /queue/{job-name}/…
- 定期清理历史残留节点,避免节点数量过多导致性能下降。
ZooKeeper 性能优化
- 内存与文件描述符:为 ZK Server 分配足够的内存,调整操作系统的文件描述符限制(ulimit -n)。
- heapSize 和 GC:禁用堆外内存开销过大的 GC 参数,并监控 JMX 指标(后续可接入 Prometheus + Grafana)。
- 一主多从或三节点集群:保证节点之间网络稳定、延迟低。
Spring Cloud Zookeeper 客户端配置
- 重试策略:在
application.yml
中可配置retry-policy
,例如ExponentialBackoffRetry
,保证短暂网络抖动时客户端自动重连。 - 心跳与会话超时:调整
sessionTimeoutMs
、connectionTimeoutMs
等参数,以匹配应用的可用性要求。 - 动态配置刷新:若使用分布式配置,确保引入
spring-boot-starter-actuator
并开启/actuator/refresh
端点,方便手动触发配置刷新。
- 重试策略:在
故障诊断
- 常见问题包括:ZooKeeper Session 超时导致临时节点丢失、客户端 Watch 逻辑未处理导致服务发现延迟、节点数过多导致性能下降。
- 建议使用工具:
zkCli.sh
查看 ZNode 结构,ZooInspector
可视化浏览 ZNode 树;定时监控 ZooKeeper 丢失率、平均延迟、请求数等。
总结
通过本文的示例与图解,我们展示了如何使用 Spring Cloud Zookeeper 构建一个基础的分布式协调系统,主要涵盖以下三个方面:
服务注册与发现
- 依托 ZooKeeper 临时顺序节点与 Watch 机制,实现实例自动上下线与负载均衡。
- 利用 Spring Cloud Zookeeper 的
@EnableDiscoveryClient
与RestTemplate
(@LoadBalanced
)让调用更为简单透明。
分布式配置中心
- 将配置信息存放在 ZooKeeper 路径之下,Spring Cloud 在启动时从 ZooKeeper 拉取并注入到环境中。
- 通过
@RefreshScope
与/actuator/refresh
实现动态刷新,保证配置修改无需重启即可生效。
分布式锁
- 基于 Apache Curator 封装的
InterProcessMutex
,让我们无需关心 ZooKeeper 底层的顺序临时节点创建与 Watch 逻辑,只需调用acquire()
/release()
即可实现锁。 - 在高并发或分布式任务场景下,通过 ZooKeeper 保证互斥访问,保证业务正确性。
- 基于 Apache Curator 封装的
除此之外,ZooKeeper 还可支持分布式队列、Leader 选举、Barrier 等更多场景,但核心思想离不开其“一致性”、“顺序节点”和“Watch 机制”。Spring Cloud Zookeeper 将这些能力以极低的使用门槛集成到 Spring Boot 应用中,让我们可以专注于业务逻辑,而不是去实现分布式协调的底层复杂度。
后续拓展方向
- 分布式队列:基于 ZooKeeper Sequential Node 实现生产者-消费者队列。
- Leader 选举:使用 Curator 提供的 LeaderSelector,确保集群中只有一个主节点在做特定任务。
- Service Mesh 与 Zookeeper:与 Istio、Envoy 等技术对比,探索更灵活的服务治理方案。
- Spring Cloud Alibaba Nacos / Consul 对比:了解 Zookeeper 相对其他注册中心(如 Nacos、Consul、Eureka)的优劣势。
通过掌握本篇内容,相信你可以在自己的项目中快速导入 Spring Cloud Zookeeper,实现服务治理、配置管理和分布式锁等功能,全面提升微服务集群的稳定性与可运维性。
评论已关闭