侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

设计一个基于ZooKeeper的分布式锁并解决羊群效应

2025-12-7 / 0 评论 / 4 阅读

题目

设计一个基于ZooKeeper的分布式锁并解决羊群效应

信息

  • 类型:问答
  • 难度:⭐⭐

考点

ZooKeeper临时顺序节点, 分布式锁原理, 锁的公平性与羊群效应

快速回答

实现分布式锁的核心步骤:

  1. 在锁节点(如/lock)下创建临时顺序节点
  2. 获取锁节点下所有子节点并排序
  3. 若当前节点是序号最小的节点,则获取锁
  4. 若非最小节点,则:
    • 监听前一个序号节点的删除事件
    • 收到通知后重新检查序号
  5. 释放锁时删除自身节点

解决羊群效应的关键:每个节点只监听前一个节点,避免所有节点监听同一个节点。

解析

一、分布式锁原理

ZooKeeper通过临时顺序节点实现分布式锁:

  • 临时节点:客户端断开连接时自动删除,避免死锁
  • 顺序节点:ZooKeeper自动追加全局唯一序号(如lock-00000001
  • 监听机制:通过Watcher监听节点变化实现阻塞唤醒

二、核心代码示例(Java)

// 创建锁节点
String lockPath = zk.create("/lock/lock-", 
                    null, 
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

// 获取所有子节点并排序
List<String> children = zk.getChildren("/lock", false);
Collections.sort(children);

// 提取当前节点序号
String currentNode = lockPath.substring("/lock/".length());
int currentIndex = children.indexOf(currentNode);

if (currentIndex == 0) {
    // 当前是最小节点,获得锁
    return true;
} else {
    // 监听前一个节点
    String prevNode = children.get(currentIndex - 1);
    CountDownLatch latch = new CountDownLatch(1);
    Stat stat = zk.exists("/lock/" + prevNode, 
        event -> {
            if (event.getType() == EventType.NodeDeleted) {
                latch.countDown();
            }
        });

    if (stat != null) {
        latch.await(); // 阻塞等待前一个节点释放
    }
    return true; // 重新检查后获得锁
}

三、羊群效应与解决方案

问题描述:若所有节点都监听最小节点,当锁释放时,所有客户端被唤醒并竞争资源,导致网络风暴。

解决方案

  • 每个节点只监听前一个序号节点
  • 只有前一个节点释放时,当前节点才被唤醒
  • 时间复杂度从O(n)降至O(1)

四、最佳实践

  1. 锁路径设计:使用明确前缀(如/app/locks/order_lock
  2. 重试机制:添加超时和重试次数限制
  3. 锁释放:确保finally块中删除节点
  4. 会话超时:合理设置sessionTimeout(建议3-30秒)

五、常见错误

  • 错误1:未处理连接断开——使用重试机制和临时节点
  • 错误2:未排序节点——必须按序号排序
  • 错误3:未监听前节点——导致羊群效应
  • 错误4:未处理节点不存在——检查exists()返回值

六、扩展知识

  • Curator框架:推荐使用其InterProcessMutex实现(已解决上述问题)
  • 锁类型:读写锁(InterProcessReadWriteLock
  • ZooKeeper特性:顺序一致性保证锁公平性
  • 对比其他方案:Redis锁(AP,无公平性)vs ZooKeeper锁(CP,强一致性)