用ZooKeeper实现分布式锁的案例?

wen java案例 68

用ZooKeeper实现分布式锁的完整案例与深度解析

📚 文章目录导读

  1. 为什么需要分布式锁?——从一个并发事故说起
  2. ZooKeeper实现分布式锁的核心原理
  3. 手写一个ZooKeeper分布式锁(Java完整代码)
  4. 关键细节:临时顺序节点、Watch机制与羊群效应
  5. 基准测试:ZooKeeper锁 vs Redis锁性能对比
  6. 常见问题与避坑指南(问答环节)
  7. 总结与最佳实践建议

为什么需要分布式锁?——从一个并发事故说起

假设你正在运营一个电商平台的秒杀系统,当1000个用户同时抢购仅剩10件的商品时,如果单机环境,使用synchronizedReentrantLock就能轻松解决,但当你将应用扩展到3台服务器,情况就变了——每台服务器上的本地锁只能锁住本机的线程,无法限制其他服务器同时扣减库存。

用ZooKeeper实现分布式锁的案例?

这就是典型的分布式场景下的并发问题:多个进程(不同机器)需要对同一共享资源(如数据库中的库存记录)进行互斥访问。

传统方案的痛点

  • 数据库悲观锁(SELECT ... FOR UPDATE):性能差,容易死锁
  • Redis分布式锁(SETNX):依赖过期时间,可能误删锁,主从切换时存在锁丢失风险
  • 基于ZooKeeper的分布式锁:强一致性、可靠性高、无需担心锁超时误删

ZooKeeper实现分布式锁的核心原理

ZooKeeper实现分布式锁依赖两个关键特性:

1 临时顺序节点(EPHEMERAL_SEQUENTIAL)

  • 临时性:当客户端会话断开后,节点自动删除,避免死锁
  • 顺序性:每个节点自动获得一个递增的序号,如lock-00000001

2 Watch机制

客户端可以监听某个节点的变化,一旦节点状态变更(如被删除),ZooKeeper会通知所有监听的客户端。

经典算法:独享锁(排他锁)

  1. 在ZooKeeper的指定路径下创建一个临时顺序节点(如/locks/lock-00000001
  2. 获取当前路径下所有子节点,按序号排序
  3. 如果自己的节点序号最小,则获得锁
  4. 否则,监听自己的前一个节点(序号比自己小一的节点)的删除事件
  5. 当监听到前一个节点被删除,重复步骤2-3

优点:避免了“羊群效应”——每个客户端只监听前一个节点,而不是监听所有节点。


手写一个ZooKeeper分布式锁(Java完整代码)

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ZKDistributedLock implements AutoCloseable {
    private static final String LOCK_ROOT = "/locks";
    private final ZooKeeper zooKeeper;
    private final String lockName;
    private String currentPath;
    private String waitPath;
    private CountDownLatch latch;
    public ZKDistributedLock(String connectString, String lockName) throws Exception {
        this.lockName = lockName;
        this.zooKeeper = new ZooKeeper(connectString, 3000, event -> {
            if (event.getType() == Watcher.Event.EventType.None && event.getState() == Watcher.Event.KeeperState.SyncConnected) {
                latch.countDown();
            }
        });
        this.latch = new CountDownLatch(1);
        latch.await(); // 等待连接建立
        ensureRootPath();
    }
    private void ensureRootPath() throws KeeperException, InterruptedException {
        Stat stat = zooKeeper.exists(LOCK_ROOT, false);
        if (stat == null) {
            zooKeeper.create(LOCK_ROOT, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }
    public boolean tryLock(long timeout, TimeUnit unit) throws Exception {
        // 1. 创建临时顺序节点
        currentPath = zooKeeper.create(LOCK_ROOT + "/" + lockName + "-", new byte[0],
                ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        long start = System.currentTimeMillis();
        long waitMs = unit.toMillis(timeout);
        // 2. 尝试获取锁
        if (attemptLock()) {
            return true;
        }
        // 3. 超时等待
        long remaining = waitMs - (System.currentTimeMillis() - start);
        while (remaining > 0) {
            if (latch != null) {
                latch.await(remaining, TimeUnit.MILLISECONDS);
            }
            if (attemptLock()) {
                return true;
            }
            remaining = waitMs - (System.currentTimeMillis() - start);
        }
        return false;
    }
    private boolean attemptLock() throws KeeperException, InterruptedException {
        List<String> children = zooKeeper.getChildren(LOCK_ROOT, false);
        Collections.sort(children);
        String currentShortPath = currentPath.substring(currentPath.lastIndexOf("/") + 1);
        int index = children.indexOf(currentShortPath);
        if (index == 0) {
            // 当前节点是最小节点,获得锁
            return true;
        }
        // 监听前一个节点
        waitPath = LOCK_ROOT + "/" + children.get(index - 1);
        Stat stat = zooKeeper.exists(waitPath, event -> {
            if (event.getType() == Watcher.Event.EventType.NodeDeleted && latch != null) {
                latch.countDown();
            }
        });
        if (stat == null) {
            // 前一个节点已不存在,重新尝试获取锁
            return attemptLock();
        }
        return false;
    }
    @Override
    public void close() throws Exception {
        if (currentPath != null) {
            zooKeeper.delete(currentPath, -1);
        }
        zooKeeper.close();
    }
}

使用示例

public class OrderService {
    public void createOrder(String orderId) {
        try (ZKDistributedLock lock = new ZKDistributedLock("localhost:2181", "order-lock")) {
            boolean locked = lock.tryLock(5, TimeUnit.SECONDS);
            if (locked) {
                // 执行业务逻辑
                System.out.println("获得锁,处理订单:" + orderId);
                Thread.sleep(100);
            } else {
                System.out.println("获取锁超时,订单处理失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

关键细节:临时顺序节点、Watch机制与羊群效应

1 为什么使用临时节点而非持久节点?

  • 如果客户端崩溃,临时节点自动删除,锁自动释放
  • 避免因客户端宕机导致锁永远无法释放的死锁问题

2 Watch机制的正确使用

  • 一次性触发:每个Watcher只能触发一次,触发后需要重新注册
  • 监听前一个节点:只监听比自己的节点,避免“羊群效应”
  • 节点删除事件:只有前一个节点被删除时,当前节点才可能获得锁

3 什么是羊群效应?

如果所有等待锁的客户端都监听根节点/locks的子节点变化,那么当锁释放时,ZooKeeper需要同时通知所有客户端,造成大量不必要的通知和竞争,而我们的算法中,每个客户端只监听前一个节点,锁释放时只通知下一个等待的客户端,复杂度从O(n)降到O(1)。


基准测试:ZooKeeper锁 vs Redis锁性能对比

对比项 ZooKeeper分布式锁 Redis分布式锁(Redlock)
一致性模型 强一致性(ZAB协议) 最终一致性(可能因主从切换丢失锁)
锁超时处理 会话超时自动释放 必须设置合理过期时间
平均获取锁延迟 约5-15ms(ZooKeeper自身延迟) 约1-5ms(内存操作)
高并发吞吐量 约2000 QPS(单机ZooKeeper) 约10000 QPS(单机Redis)
可靠性 极高(3节点集群保证) 中等(依赖Redis集群状态)

选择建议

  • 对一致性要求极高的场景(如金融交易、库存扣减)→ 选择ZooKeeper
  • 对性能要求极高且允许短暂不一致的场景(如社交点赞、缓存刷新)→ 选择Redis

常见问题与避坑指南(问答环节)

❓ Q1:ZooKeeper集群挂掉怎么办?

分布式锁本身依赖ZooKeeper的可用性,建议部署3或5节点集群,容忍少数节点故障,如果所有ZooKeeper节点不可用,业务方需要有降级策略(如:暂停写入、等待恢复)。

❓ Q2:客户端会话超时导致锁自动释放,但业务还没执行完怎么办?

这是ZooKeeper分布式锁的根本缺陷,解决方案:

  1. 合理设置会话超时时间(通常2-10秒)
  2. 为业务代码添加超时保护,确保业务在锁有效期内完成
  3. 使用“锁续约”机制(类似Redisson的Watch Dog)

❓ Q3:锁被其他客户端删除了怎么办?

我们的实现中只监听前一个节点,且只有自己持有锁时才执行删除,但需注意:如果客户端A的会话断开后锁自动删除,客户端B获取锁后,客户端A的代码还在继续执行——这时需要业务方做好幂等性校验。

❓ Q4:大量客户端同时争抢锁,ZooKeeper性能会下降吗?

由于每个客户端只监听前一个节点,ZooKeeper不需要广播通知,性能相对可控,但在极端情况下(如10万个客户端排队),ZooKeeper的节点创建和Watch注册仍会造成压力,建议通过“分段锁”降低竞争粒度。


总结与最佳实践建议

适用场景最佳实践

  1. 使用Curator框架:Apache Curator对ZooKeeper分布式锁进行了封装,提供了InterProcessMutex等现成实现,无需重复造轮子
  2. 设定合理的会话超时:通常设为业务最大执行时间的2倍
  3. 配合业务重试机制:获取锁失败时,使用指数退避策略重试
  4. 警惕网络抖动:在网络不稳定的环境下,建议使用“可重入锁”或“读写锁”

何时考虑替代方案

  • 如果业务对性能要求极高(>5000 QPS),考虑使用Redis分布式锁
  • 如果业务允许短暂不一致(如缓存更新),直接使用数据库乐观锁即可
  • 如果业务涉及跨数据中心部署,考虑使用Etcd(基于Raft协议)实现分布式锁

最后一点提醒:没有任何一种分布式锁方案是银弹,在选择技术方案时,请务必结合你的业务场景、团队技术栈、运维能力综合评估,ZooKeeper实现分布式锁虽然复杂但稳定可靠,适合对数据一致性有严苛要求的核心业务系统。

抱歉,评论功能暂时关闭!