目录

分布式专题17-ZooKeeper经典应用场景实战下

分布式专题——17 ZooKeeper经典应用场景实战(下)

[https://csdnimg.cn/release/blogv2/dist/pc/img/activeVector.png 『Java分布式系统开发:从理论到实践』征文活动 10w+人浏览 305人参与

https://csdnimg.cn/release/blogv2/dist/pc/img/arrowright-line-White.png]( )

1 ZooKeeper 分布式锁实战

1.1 什么是分布式锁

  • 单体应用开发场景中,涉及并发同步时,通常使用 Synchronized(同步)或其他同一 JVM 内的 Lock 机制来解决多线程间的同步问题;

  • 分布式集群工作的开发场景下,需要一种更高级的锁机制来处理跨机器的进程之间的数据同步问题,这种跨机器的锁就是分布式锁

  • 目前比较成熟、主流的分布式锁方案有以下几种:

    • 基于数据库的分布式锁:利用数据库的事务和锁机制来实现分布式锁。虽然在某些场景下能实现简单的分布式锁,但由于数据库操作性能相对较低,还可能面临锁表的风险,所以一般不是首选方案;
    • 基于 Redis 的分布式锁:Redis 分布式锁是一种常见且成熟的方案,适用于高并发、性能要求高且可靠性问题可通过其他方式弥补的场景。Redis 提供了高效的内存存储和原子操作,能快速获取和释放锁,在大规模分布式系统中得到广泛应用;
    • 基于 ZooKeeper 的分布式锁:这种方案适用于对高可靠性和一致性要求较高,而并发量不是太高的场景。因为 ZooKeeper 具有强一致性保证,所以能较好地满足对可靠性和一致性要求高的分布式锁需求。

1.2 基于数据库实现分布式锁的设计思路

  • 利用数据库的唯一索引来实现分布式锁,因为唯一索引天然具有排他性。具体流程如下:

    • 开始;

    • 插入一条数据,其中 method name 字段具有唯一约束;

    • 判断数据是否插入成功:

      • 如果插入成功,说明获取到了锁;
      • 如果插入失败,说明锁已被其他进程获取,此时需要等待,当对应的数据被删除后,再去尝试获取锁;
    • 当获取到锁并完成相关操作后,释放锁,即删除之前插入的数据;

    • 结束;

    https://i-blog.csdnimg.cn/direct/f8a68a95aa5e41d0a7160fff1470dd2f.png

  • 基于数据库实现分布式锁存在一些问题,比如:

    • 性能问题:数据库操作相对较慢,尤其是在高并发场景下,频繁的数据库插入、查询、删除操作会导致性能瓶颈;
    • 可靠性问题:如果获取锁的进程在释放锁之前出现异常(如宕机),可能导致数据无法被删除,从而造成锁无法释放,出现死锁情况;
    • 扩展性问题:对于分布式系统来说,数据库本身的扩展性可能有限,当系统规模扩大时,可能无法很好地支撑大量的锁操作请求。

1.3 基于 ZooKeeper 实现分布式锁设计思路一

  • 核心逻辑:使用临时 ZNode(/lock)来表示获取锁的请求,创建 ZNode 成功的用户拿到锁;

  • 流程

    • 开始尝试创建临时节点 /lock
    • 判断是否创建成功:
      • 如果创建成功,获取锁;之后删除 /lock 节点释放锁,流程结束;
      • 如果创建失败,监听 /lock 节点,并等待。当 /lock 节点被删除时,会收到通知,再次尝试创建 /lock 节点以获取锁;

    https://i-blog.csdnimg.cn/direct/8ee130350ae041ed9fbbaa32e9bbc2d0.png

  • 存在的问题:如果所有的锁请求者都监听锁持有者(即代表锁持有者的 ZNode),当该 ZNode 被删除后,所有锁请求者都会收到通知,但只有一个锁请求者能拿到锁。这会导致羊群效应,即多个客户端同时被唤醒,但只有一个能获取锁,其他客户端又会继续等待,造成不必要的资源消耗。

1.4 基于 ZooKeeper 实现分布式锁设计思路二

  • 核心逻辑:使用临时有序 ZNode 来表示获取锁的请求,创建最小后缀数字 ZNode 的用户成功拿到锁,实现了公平锁(按照请求顺序获取锁);

  • 流程

    • 开始在 /locks 下创建临时有序节点;
    • 获取 /locks 下的所有子节点,并将子节点按序号由小到大排序;
    • 判断当前节点序号是否最小:
      • 如果是,获取锁;之后删除该节点释放锁,流程结束;
      • 如果不是,只需要监听比自己序号小 1 的节点的删除事件。当比自己序号小 1 的节点被删除时,会收到通知,再次检查自己是否是当前序号最小的节点,若是则获取锁;

    https://i-blog.csdnimg.cn/direct/5b9323644d8b4343a4795c67d3968532.png

  • 开发建议:在实际开发中,如果需要使用分布式锁,不建议自己“重复造轮子”,建议直接使用 Curator 客户端中的各种官方实现的分布式锁,例如其中的 InterProcessMutex 可重入锁,这样能避免自己实现过程中可能出现的各种问题,提高开发效率和可靠性。

1.5 Curator 可重入分布式锁工作流程

https://i-blog.csdnimg.cn/direct/219d3a8d5e594d25874f0117f6bfea35.png

  • acquire(获取锁)流程

    • 判断当前线程是否已获取锁

      • 若已获取锁:重入次数加 1,直接获取成功。同时,会将当前线程和锁信息存入 ConcurrentHashMap,标记重入次数为 1(首次重入时);
      • 若未获取锁:进入后续创建节点的流程;
    • 创建临时有序子节点:在锁节点下创建一个临时有序子节点;

    • 判断节点创建是否成功

      • 若成功:
        • 检查是否是第一个子节点:
          • 若是:获取锁成功,将当前线程和锁信息存入 ConcurrentHashMap,重入次数记为 1;
          • 若不是:获取前一个节点的路径,并为该前序节点添加 watcher(监听器)。之后当前线程进入阻塞状态(wait()),等待被唤醒(notifyAll());
      • 若失败:判断是否超过重试次数
        • 未超过:再次尝试创建临时有序子节点;
        • 超过:抛出异常,获取锁失败;
  • release(释放锁)流程

    • 获取当前线程锁信息,重入次数减 1

    • 判断重入次数

      • 若重入次数仍大于 0:说明未完全释放重入,直接返回(锁仍由当前线程持有,只是重入层级减少);
      • 若重入次数小于 0:抛出异常(逻辑上不可能出现,属于异常情况);
      • 若重入次数等于 0:
        • 删除锁对应的节点;
        • 删除 ConcurrentHashMap 中该线程和锁的信息;
        • 触发 watcher,唤醒等待该锁的线程(notifyAll()),完成释放,标记释放完成;
  • 关键知识点

    • 可重入性:通过 ConcurrentHashMap 记录线程的重入次数,支持同一线程多次获取锁,每次重入次数递增;释放时次数递减,直到次数为 0 才真正释放锁;
    • 临时有序节点:利用 ZooKeeper 临时有序节点的特性,保证锁的公平性(按节点创建顺序排队),且节点随会话结束自动删除,避免死锁;
    • 监听器(Watcher):为前序节点添加监听器,当前序节点被删除(锁释放)时,触发通知,唤醒当前阻塞的线程重新尝试获取锁;
    • 重试机制:创建节点失败时,支持重试,避免因网络波动等临时问题导致获取锁失败;
    • 线程同步:通过 wait()notifyAll() 实现线程间的等待与唤醒,保证锁的有序获取。

1.6 小结

  • 优点:ZooKeeper 分布式锁具备高可用、可重入、阻塞锁的特性

    • 高可用:ZooKeeper 本身是分布式协调服务,具有良好的集群特性,能保证在集群环境下锁服务的可用性;
    • 可重入:支持同一线程多次获取同一把锁,就像本地的可重入锁(如 ReentrantLock)一样,避免了同一线程因重复获取锁而出现死锁的情况;
    • 阻塞锁特性:当线程获取锁失败时,会进入阻塞状态,等待锁释放后再尝试获取,保证了线程获取锁的有序性;
    • 解决失效死锁问题:由于 ZooKeeper 临时节点的特性,当持有锁的客户端会话失效(如宕机)时,临时节点会被自动删除,锁也会被释放,从而避免了因客户端异常导致的死锁问题;
    • 使用简单:Curator 等客户端封装了 ZooKeeper 分布式锁的实现,开发者可以方便地调用相关 API 来使用分布式锁;
  • 缺点:因为在使用过程中需要频繁地创建和删除 ZooKeeper 节点,而 ZooKeeper 节点的操作相对 Redis 等基于内存操作的组件来说,开销更大,所以性能上不如 Redis 分布式锁;

  • 适用场景建议:

    • 在高性能、高并发的应用场景下,不建议使用 ZooKeeper 的分布式锁,因为其性能可能无法满足高并发下的性能要求,此时 Redis 分布式锁可能是更优选择;
    • 由于 ZooKeeper 具有高可靠性,所以在并发量不是太高的应用场景中,推荐使用 ZooKeeper 的分布式锁,能在保证可靠性的同时,满足锁的使用需求。

2 基于 ZooKeeper 实现服务的注册与发现

2.1 设计思路

https://i-blog.csdnimg.cn/direct/8cfe785bf2d34119836548a98c025fef.png

  1. 服务注册:服务(如 Server1Server2Server3)启动时,会在 ZooKeeper 集群的 /services 节点下创建临时节点(如 /services/server1/services/server2/services/server3),并将自身的 ip:port 等信息存储在节点中;
  2. 服务发现(客户端初始操作):客户端(如 Client1Client2Client3)会获取当前 /services 节点下的所有子节点,从而得到当前在线的服务列表,并且会注册对这些子节点的监听(Watcher);
  3. 服务下线:当某一服务节点(如 Server2)下线时,其在 ZooKeeper 上对应的临时节点会被删除;
  4. 下线通知:ZooKeeper 会将服务节点下线的事件通知给监听该节点的客户端;
  5. 客户端更新服务列表:客户端收到通知后,会重新获取 /services 节点下的子节点,更新服务列表,并且再次注册监听,以感知后续服务节点的变化。

2.2 优缺点

  • 优点:

    • 高可用性:ZooKeeper 是高可用的分布式系统,可通过配置多个服务器实例提供容错能力。若其中一个实例出现故障,其他实例仍能继续提供服务,保障服务注册与发现功能的持续可用;
    • 强一致性:ZooKeeper 能保证数据的强一致性,当有更新操作完成时,所有服务器都会有相同的数据视图。这使得 ZooKeeper 作为服务注册中心时,可确保所有客户端看到的服务状态一致,避免因数据不一致导致的服务调用问题;
    • 实时性:ZooKeeper 的监听器(Watcher)机制允许客户端监听节点变化。当服务提供者状态(上线或下线)改变时,客户端会实时收到通知,使服务消费者能快速响应服务变化,实现动态服务发现;
  • 缺点:

    • 性能限制:ZooKeeper 的性能可能不如一些专为服务注册中心设计的解决方案(如 Nacos、Consul)。特别是在大量读写操作或大规模集群场景下,ZooKeeper 可能会遇到性能瓶颈,难以高效处理高并发的服务注册与发现请求。

2.3 整合SpringCloud ZooKeeper实现微服务注册中心

  • 在父pom文件中指定 SpringCloud 版本:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</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>
  • 微服务pom文件中引入 SpringCloud ZooKeeper 注册中心依赖:

    <!-- ZooKeeper 服务注册与发现 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.apache.zookeeper</groupId>
                <artifactId>zookeeper</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- ZooKeeper client -->
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.8.0</version>
    </dependency>
    • 注意: ZooKeeper 客户端依赖和 ZooKeeper sever 的版本兼容问题;
    • SpringCloud整合 ZooKeeper 注册中心核心源码入口: ZookeeperDiscoveryClientConfiguration
  • 微服务配置文件application.yml中配置 ZooKeeper 注册中心地址:

    spring:
      cloud:
        zookeeper:    
          connect-string: localhost:2181
          discovery:
            instance-host: 127.0.0.1
  • 整合 feign 进行服务调用:

    @RequestMapping(value = "/findOrderByUserId/{id}")
    public R  findOrderByUserId(@PathVariable("id") Integer id) {
        log.info("根据userId:"+id+"查询订单信息");
        // feign调用   
        R result = orderFeignService.findOrderByUserId(id);
        return result;
    }
  • 测试:http://localhost:8040/user/findOrderByUserId/1