前期, 我们介绍了什么是分布式锁及分布式锁应用场景,并分享了基于Redis方案实现的分布式锁, 今天我们基于Zookeeper方案来实现分布式锁的应用。
一. 方案概述
1.1. 实现原理:
- 临时顺序节点:每个客户端请求锁时,在ZooKeeper的指定节点下创建一个临时顺序节点。
- 锁竞争机制:
- 客户端创建节点后,获取所有子节点列表并排序
- 如果自己创建的节点是序号最小的节点,则获得锁
- 否则,监听前一个节点的删除事件,进入等待状态
- 自动释放:客户端会话结束(如崩溃)时,临时节点自动删除,后续节点自动获得锁

1.2 核心优势:
- 强一致性:基于ZooKeeper的原子广播协议(ZAB),保证锁的可靠性
- 高可用:集群模式下部分节点故障不影响锁服务
- 避免死锁:临时节点特性确保客户端崩溃后锁自动释放
- 公平锁:通过顺序节点天然实现FIFO的公平性
1.3 使用场景:
- 对一致性要求极高的场景(如分布式事务协调)
- 避免单点故障的高可用场景
- 需要公平锁特性的场景
二. 代码实现
以下是基于Zookeeper方案的分布式锁的重要实现代码片段(仅供参考)。
首先,我们需要引入ZooKeeper客户端依赖:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.8.0</version>
</dependency>
ZooKeeperDistrubutedLock.java
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 基于ZooKeeper的分布式锁实现
*/
public class ZooKeeperDistributedLock implements Lock, Watcher {
private ZooKeeper zk;
private String root = "/distributed_locks";
private String lockName;
private String waitNode;
private String myNode;
private CountDownLatch latch;
private CountDownLatch connectedLatch = new CountDownLatch(1);
private int sessionTimeout = 30000;
/**
* 创建分布式锁
* @param connectString ZooKeeper连接地址
* @param lockName 锁名称
*/
public ZooKeeperDistributedLock(String connectString, String lockName) {
this.lockName = lockName;
try {
// 创建连接
zk = new ZooKeeper(connectString, sessionTimeout, this);
connectedLatch.await();
// 检查根节点
Stat stat = zk.exists(root, false);
if (stat == null) {
// 创建根节点
zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (IOException | InterruptedException | KeeperException e) {
throw new RuntimeException(e);
}
}
@Override
public void process(WatchedEvent event) {
// 连接建立时
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedLatch.countDown();
return;
}
// 等待的节点被删除时
if (this.latch != null) {
this.latch.countDown();
}
}
@Override
public void lock() {
try {
if (this.tryLock()) {
return;
} else {
// 等待锁
waitForLock(waitNode, sessionTimeout);
}
} catch (KeeperException | InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
this.lock();
}
@Override
public boolean tryLock() {
try {
// 创建临时顺序节点
myNode = zk.create(root + "/" + lockName, new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有子节点
List<String> children = zk.getChildren(root, false);
// 排序
Collections.sort(children);
if (myNode.equals(root + "/" + children.get(0))) {
// 如果是第一个节点,则获取锁成功
return true;
}
// 否则,获取前一个节点作为等待节点
String subNode = myNode.substring((root + "/").length());
int idx = Collections.binarySearch(children, subNode);
waitNode = root + "/" + children.get(idx - 1);
} catch (KeeperException | InterruptedException e) {
throw new RuntimeException(e);
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
try {
if (this.tryLock()) {
return true;
}
return waitForLock(waitNode, unit.toMillis(time));
} catch (KeeperException e) {
e.printStackTrace();
}
return false;
}
private boolean waitForLock(String lowerNode, long waitTime)
throws KeeperException, InterruptedException {
// 监听前一个节点
Stat stat = zk.exists(lowerNode, true);
if (stat != null) {
this.latch = new CountDownLatch(1);
// 等待前一个节点释放锁
boolean result = this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
if (!result) {
// 等待超时,检查自己是否是最小节点
List<String> children = zk.getChildren(root, false);
Collections.sort(children);
String currentNode = myNode.substring((root + "/").length());
int idx = Collections.binarySearch(children, currentNode);
if (idx == 0) {
return true;
}
return false;
}
}
return true;
}
@Override
public void unlock() {
try {
zk.delete(myNode, -1);
myNode = null;
zk.close();
} catch (InterruptedException | KeeperException e) {
throw new RuntimeException(e);
}
}
@Override
public Condition newCondition() {
return null;
}
}
ZooKeeperLockExample.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* ZooKeeper分布式锁使用示例
*/
public class ZooKeeperLockExample {
private static final String ZK_CONNECT_STRING = "localhost:2181";
private static final String LOCK_NAME = "my_lock";
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
ZooKeeperDistributedLock lock = null;
try {
// 创建锁实例
lock = new ZooKeeperDistributedLock(ZK_CONNECT_STRING, LOCK_NAME);
// 尝试获取锁(带超时)
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁");
// 模拟业务操作
Thread.sleep(2000);
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock != null) {
lock.unlock();
}
}
});
}
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
三. 注意事项
基于 ZooKeeper 实现分布式锁时,需重点关注连接可靠性、节点监听策略、异常处理和性能优化。合理利用 Curator 等成熟框架,避免重复造轮子,同时结合业务场景调整参数(如会话超时、重试策略),以达到最佳效果。
3.1. 连接管理与会话可靠性
-
连接丢失处理
ZooKeeper客户端与服务器的连接可能因网络抖动而中断,导致会话过期。需确保:- 使用
Watch机制监听NodeDeleted事件,当前一个节点被删除时重新竞争锁。 - 实现连接状态监听,在连接恢复后重新获取锁。
- 使用
-
会话超时设置
会话超时时间(sessionTimeout)应根据业务执行时间合理设置:- 过短:可能因网络延迟导致会话提前过期,锁被误释放。
- 过长:客户端崩溃后锁释放延迟过长。
// 示例:设置合理的会话超时和重试策略 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("localhost:2181") .sessionTimeoutMs(30000) // 30秒会话超时 .connectionTimeoutMs(5000) .retryPolicy(retryPolicy) .build();
3.2. 临时顺序节点的正确使用
-
节点路径清理
业务完成后需主动删除临时节点,避免无用节点堆积。若使用Curator框架,InterProcessMutex会自动处理。 -
节点监听优化
仅监听前一个节点,而非所有节点,避免“羊群效应”(Herd Effect):// 错误示例:监听所有子节点(导致大量Watcher触发) client.getChildren().watched().forPath(lockPath); // 正确示例:仅监听前一个节点 String myNode = createEphemeralSequentialNode(); String prevNode = getPreviousNode(myNode); if (prevNode != null) { client.getData().usingWatcher(myWatcher).forPath(prevNode); }
3.3. 异常处理与幂等性
-
解锁的幂等性
避免重复解锁导致其他客户端的锁被误释放:// 解锁前检查锁是否仍被当前客户端持有 public void unlock() { if (isOwner()) { // 检查是否为锁的持有者 deleteNode(); } } -
异常恢复
捕获并处理KeeperException、InterruptedException等异常,确保锁状态一致性:try { lock.acquire(); // 业务逻辑 } catch (KeeperException | InterruptedException e) { Thread.currentThread().interrupt(); // 处理异常,考虑释放锁 } finally { lock.releaseIfHeld(); // 安全释放锁的方法 }
3.4. 性能与资源优化
-
连接池限制
避免创建过多ZooKeeper客户端连接,建议使用连接池:// 使用CuratorFramework作为单例管理连接 private static final CuratorFramework client = createSingletonClient(); -
减少不必要的Watch
Watch是一次性触发的,需在事件处理后重新注册,避免遗漏监听:private void watchPreviousNode(String prevNode) { client.getData().usingWatcher(new Watcher() { @Override public void process(WatchedEvent event) { if (event.getType() == Event.EventType.NodeDeleted) { tryAcquireLock(); // 前一个节点删除,尝试获取锁 watchPreviousNode(findNewPreviousNode()); // 重新注册Watch } } }).forPath(prevNode); }
3.5. 可重入性与公平性
-
实现可重入锁
记录当前线程的获取次数,同一线程可多次获取锁:private final ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0); public boolean tryLock() { if (isHeldByCurrentThread()) { lockCount.set(lockCount.get() + 1); return true; } // 正常获取锁逻辑 } -
公平锁保证
ZooKeeper的顺序节点天然支持公平性,先创建的节点优先获得锁。
3.6. 避免常见陷阱
-
羊群效应(Herd Effect)
多个客户端监听同一节点,节点删除时会同时唤醒所有客户端,导致性能骤降。应监听前一个节点而非根节点。 -
会话过期处理
当会话过期时,临时节点自动删除,但客户端可能仍认为持有锁。需通过状态检查或重连机制解决:// 监听连接状态 client.getConnectionStateListenable().addListener((client, newState) -> { if (newState == ConnectionState.LOST) { // 重置锁状态 resetLockStatus(); } });
3.7. 生产环境建议
-
使用成熟框架
避免手写底层逻辑,推荐使用Apache Curator框架:// Curator的可重入锁示例 InterProcessMutex lock = new InterProcessMutex(client, "/distributed-lock"); try { if (lock.acquire(10, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { if (lock.isAcquiredInThisProcess()) { lock.release(); } } -
监控与告警
监控ZooKeeper集群状态、锁等待时间、竞争激烈程度等指标,及时发现性能瓶颈。

156

被折叠的 条评论
为什么被折叠?



