1. ZooKeeper 是什么

ZooKeeper 是一个分布式协调服务,常用于:

  • 服务注册与发现
  • 分布式配置管理
  • 分布式锁
  • Leader 选举
  • 集群节点状态监听

它的核心数据结构是 znode,可以理解成一棵类似文件系统的树。每个节点既可以存数据,也可以挂子节点。

常见节点类型:

类型说明典型用途
持久节点客户端断开后仍然存在配置、目录
临时节点客户端会话结束后自动删除服务实例注册
顺序节点创建时自动追加递增序号分布式锁、队列
临时顺序节点会话结束自动删除,并带顺序号分布式锁、Leader 选举

在 Spring Boot 里使用 ZooKeeper,常见有两种方式:

  1. 使用 Spring Cloud Zookeeper 做服务发现和配置中心。
  2. 使用 Apache Curator 操作节点、监听节点、实现分布式锁。

2. 环境准备

本文以 Spring Boot 3.5.x 和 Spring Cloud 2025.0.x 为例。

如果你使用 Spring Boot 4.0.x,应选择 Spring Cloud 2025.1.x。Spring Cloud 和 Spring Boot 必须匹配版本,不建议手动乱指定 starter 版本。

先用 Docker 启动一个本地 ZooKeeper:

# docker-compose.yml
services:
  zookeeper:
    image: zookeeper:3.9
    container_name: zk-blog
    ports:
      - "2181:2181"
    environment:
      ZOO_4LW_COMMANDS_WHITELIST: "ruok,stat,mntr"

启动:

docker compose up -d

验证:

docker exec -it zk-blog zkCli.sh

进入 CLI 后执行:

ls /

能正常返回节点列表,说明 ZooKeeper 已经可用。


3. Maven 依赖

<properties>
    <java.version>17</java.version>
    <spring-cloud.version>2025.0.3</spring-cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <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>

<dependencies>
    <!-- Web 服务 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- ZooKeeper 服务注册与发现 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    </dependency>

    <!-- ZooKeeper 配置中心,可选 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-config</artifactId>
    </dependency>

    <!-- 客户端负载均衡 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>

    <!-- Curator 分布式锁、缓存监听等高级用法 -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
    </dependency>
</dependencies>

注意:如果使用了 Spring Cloud BOM,业务依赖一般不要再手写具体版本,交给 BOM 管理即可。


4. 服务注册

创建一个订单服务 order-service。

# application.yml
server:
  port: 8081

spring:
  application:
    name: order-service
  cloud:
    zookeeper:
      connect-string: 127.0.0.1:2181
      discovery:
        enabled: true
        register: true
        root: /services

启动类:

package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

写一个测试接口:

package com.example.order;

import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Value("${server.port}")
    private int port;

    @GetMapping("/api/orders/ping")
    public Map<String, Object> ping() {
        return Map.of(
                "service", "order-service",
                "port", port,
                "message", "order service is ok"
        );
    }
}

启动后,进入 ZooKeeper CLI:

docker exec -it zk-blog zkCli.sh

查看服务注册信息:

ls /services
ls /services/order-service

能看到 order-service 以及实例节点,就说明服务注册成功。


5. 服务发现与调用

再创建一个消费者服务 user-service。

# application.yml
server:
  port: 8082

spring:
  application:
    name: user-service
  cloud:
    zookeeper:
      connect-string: 127.0.0.1:2181
      discovery:
        enabled: true
        register: true

配置一个支持负载均衡的 RestTemplate:

package com.example.user;

import org.springframework.boot.web.client.RestTemplateBuilder;
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 HttpClientConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}

调用 order-service:

package com.example.user;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UserController {

    private final RestTemplate restTemplate;

    public UserController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/api/users/call-order")
    public String callOrder() {
        return restTemplate.getForObject(
                "http://order-service/api/orders/ping",
                String.class
        );
    }
}

访问:

curl http://localhost:8082/api/users/call-order

这里的 http://order-service 不是 DNS 域名,而是服务名。Spring Cloud LoadBalancer 会通过 ZooKeeper 找到真实实例地址。


6. 使用 ZooKeeper 做配置中心

Spring Cloud Zookeeper Config 可以把 ZooKeeper 中的节点数据加载进 Spring Environment。

配置:

server:
  port: 8081

spring:
  application:
    name: order-service
  config:
    import: optional:zookeeper:
  cloud:
    zookeeper:
      connect-string: 127.0.0.1:2181
      config:
        enabled: true
        root: config
        default-context: application
        profile-separator: ","

默认配置路径类似:

路径说明
/config/application所有应用共享配置
/config/application,dev所有应用在 dev 环境的共享配置
/config/order-serviceorder-service 专属配置
/config/order-service,devorder-service 在 dev 环境的专属配置

写入配置:

docker exec -it zk-blog zkCli.sh

在 ZooKeeper CLI 中执行:

create /config ""
create /config/application ""
create /config/order-service ""
create /config/order-service/app.message "hello from zookeeper"
create /config/order-service/order.discount-enabled "true"

读取配置:

package com.example.order;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConfigController {

    @Value("${app.message:default message}")
    private String message;

    @Value("${order.discount-enabled:false}")
    private boolean discountEnabled;

    @GetMapping("/api/config")
    public String config() {
        return "message=" + message + ", discountEnabled=" + discountEnabled;
    }
}

访问:

curl http://localhost:8081/api/config

返回示例:

message=hello from zookeeper, discountEnabled=true

7. 使用 Curator 操作 ZooKeeper 节点

如果项目已经引入了 Spring Cloud Zookeeper,通常可以直接注入 CuratorFramework。

package com.example.order;

import java.nio.charset.StandardCharsets;

import org.apache.curator.framework.CuratorFramework;
import org.springframework.stereotype.Service;

@Service
public class ZkNodeService {

    private final CuratorFramework curatorFramework;

    public ZkNodeService(CuratorFramework curatorFramework) {
        this.curatorFramework = curatorFramework;
    }

    public void createOrUpdate(String path, String value) throws Exception {
        byte[] data = value.getBytes(StandardCharsets.UTF_8);

        if (curatorFramework.checkExists().forPath(path) == null) {
            curatorFramework.create()
                    .creatingParentsIfNeeded()
                    .forPath(path, data);
            return;
        }

        curatorFramework.setData().forPath(path, data);
    }

    public String get(String path) throws Exception {
        byte[] data = curatorFramework.getData().forPath(path);
        return new String(data, StandardCharsets.UTF_8);
    }

    public void delete(String path) throws Exception {
        if (curatorFramework.checkExists().forPath(path) != null) {
            curatorFramework.delete()
                    .deletingChildrenIfNeeded()
                    .forPath(path);
        }
    }
}

测试接口:

package com.example.order;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ZkNodeController {

    private final ZkNodeService zkNodeService;

    public ZkNodeController(ZkNodeService zkNodeService) {
        this.zkNodeService = zkNodeService;
    }

    @GetMapping("/api/zk/node")
    public String node() throws Exception {
        String path = "/demo/order/message";
        zkNodeService.createOrUpdate(path, "created by spring boot");
        return zkNodeService.get(path);
    }
}

访问:

curl http://localhost:8081/api/zk/node

8. 使用 Curator 实现分布式锁

分布式锁是 ZooKeeper 最常见的应用之一。Curator 已经封装好了锁实现,推荐直接使用 InterProcessMutex,不要自己手写临时顺序节点逻辑。

package com.example.order;

import java.util.concurrent.TimeUnit;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.stereotype.Service;

@Service
public class OrderLockService {

    private final CuratorFramework curatorFramework;

    public OrderLockService(CuratorFramework curatorFramework) {
        this.curatorFramework = curatorFramework;
    }

    public String createOrder(Long userId, Long productId) throws Exception {
        String lockPath = "/locks/order-create/" + productId;
        InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);

        boolean acquired = lock.acquire(3, TimeUnit.SECONDS);
        if (!acquired) {
            return "系统繁忙,请稍后重试";
        }

        try {
            return doCreateOrder(userId, productId);
        } finally {
            lock.release();
        }
    }

    private String doCreateOrder(Long userId, Long productId) {
        return "create order success, userId=" + userId + ", productId=" + productId;
    }
}

接口:

package com.example.order;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderLockController {

    private final OrderLockService orderLockService;

    public OrderLockController(OrderLockService orderLockService) {
        this.orderLockService = orderLockService;
    }

    @GetMapping("/api/orders/create")
    public String createOrder() throws Exception {
        return orderLockService.createOrder(1001L, 2001L);
    }
}

访问:

curl http://localhost:8081/api/orders/create

分布式锁使用建议:

  • 锁路径要尽量细,例如按商品 ID、订单 ID、用户 ID 拆分,不要所有业务共用一个锁。
  • acquire 一定要设置超时时间,避免线程无限等待。
  • release 必须放在 finally 中。
  • 锁内逻辑要尽量短,不要把长时间网络调用放进锁里。
  • 分布式锁不能替代业务幂等,订单、支付、库存仍然要做唯一约束和幂等控制。

9. 监听节点变化

Curator 提供了缓存监听能力,可以监听某个路径下节点的新增、修改、删除。

package com.example.order;

import java.nio.charset.StandardCharsets;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

@Component
public class ZkNodeWatcher {

    private final CuratorFramework curatorFramework;
    private CuratorCache cache;

    public ZkNodeWatcher(CuratorFramework curatorFramework) {
        this.curatorFramework = curatorFramework;
    }

    @PostConstruct
    public void start() {
        String watchPath = "/demo/order";

        this.cache = CuratorCache.build(curatorFramework, watchPath);
        this.cache.listenable().addListener((type, oldData, data) -> {
            if (data == null) {
                System.out.println("node event: " + type);
                return;
            }

            String path = data.getPath();
            String value = data.getData() == null
                    ? ""
                    : new String(data.getData(), StandardCharsets.UTF_8);

            System.out.println("node event: " + type + ", path=" + path + ", value=" + value);
        });

        this.cache.start();
    }

    @PreDestroy
    public void stop() {
        if (this.cache != null) {
            this.cache.close();
        }
    }
}

然后调用前面的 /api/zk/node 接口修改节点,就能在控制台看到监听日志。


10. 常见问题

10.1 启动时报连不上 ZooKeeper

检查:

docker ps
docker logs zk-blog

确认 application.yml 中连接地址正确:

spring:
  cloud:
    zookeeper:
      connect-string: 127.0.0.1:2181

如果只是本地开发,可以使用:

spring:
  config:
    import: optional:zookeeper:

optional: 表示 ZooKeeper 暂时不可用时不阻止应用启动。生产环境是否使用 optional 要看业务要求。

10.2 服务没有注册进去

检查:

spring:
  cloud:
    zookeeper:
      discovery:
        enabled: true
        register: true

然后进入 CLI:

ls /services

如果没有服务节点,优先看应用启动日志和 ZooKeeper 连接日志。

10.3 配置读取不到

确认路径是否符合默认规则:

/config/application
/config/application,dev
/config/{spring.application.name}
/config/{spring.application.name},{profile}

例如应用名是:

spring:
  application:
    name: order-service

那么专属配置路径就是:

/config/order-service

10.4 ZooKeeper 可以当数据库用吗

不建议。

ZooKeeper 适合存储小体量的协调数据,例如服务实例、配置项、锁节点、Leader 状态。不要把大 JSON、大列表、业务主数据、订单数据放到 ZooKeeper 里。


11. 生产环境建议

  1. ZooKeeper 集群建议使用 3 个或 5 个节点,避免单点故障。
  2. 不要在 ZooKeeper 中存储大数据,只保存协调元信息。
  3. 对关键路径配置 ACL、网络访问控制和监控告警。
  4. 设置合理的连接超时、会话超时和重试策略。
  5. 分布式锁只保护临界区,业务仍然要有幂等、唯一索引和补偿机制。
  6. 服务发现适合内部服务治理,不要直接暴露 ZooKeeper 给公网。
  7. 配置中心场景要明确刷新策略,不要假设所有配置都会自动实时刷新到所有 Bean。

12. 总结

Spring Boot 使用 ZooKeeper 主要有三类场景:

场景推荐方案
服务注册与发现spring-cloud-starter-zookeeper-discovery
配置中心spring-cloud-starter-zookeeper-config
分布式锁、节点监听Apache Curator

如果只是做 Spring Cloud 微服务治理,优先使用 Spring Cloud Zookeeper。如果要实现锁、选主、节点监听等协调能力,优先使用 Curator 提供的成熟 recipe,不建议自己手写底层 ZooKeeper 算法。


参考资料

更多推荐