前期, 我们介绍了什么是分布式锁及分布式锁应用场景,并分享了基于Redis方案实现的分布式锁, 今天我们基于Zookeeper方案来实现分布式锁的应用。
以下是基于Zookeeper方案的分布式锁的重要实现代码片段(仅供参考)。
首先,我们需要引入ZooKeeper客户端依赖:
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.8.0version>
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 等成熟框架,避免重复造轮子,同时结合业务场景调整参数(如会话超时、重试策略),以达到最佳效果。
连接丢失处理
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();
节点路径清理
业务完成后需主动删除临时节点,避免无用节点堆积。若使用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);
}
解锁的幂等性
避免重复解锁导致其他客户端的锁被误释放:
// 解锁前检查锁是否仍被当前客户端持有
public void unlock() {
if (isOwner()) { // 检查是否为锁的持有者
deleteNode();
}
}
异常恢复
捕获并处理KeeperException
、InterruptedException
等异常,确保锁状态一致性:
try {
lock.acquire();
// 业务逻辑
} catch (KeeperException | InterruptedException e) {
Thread.currentThread().interrupt();
// 处理异常,考虑释放锁
} finally {
lock.releaseIfHeld(); // 安全释放锁的方法
}
连接池限制
避免创建过多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);
}
实现可重入锁
记录当前线程的获取次数,同一线程可多次获取锁:
private final ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);
public boolean tryLock() {
if (isHeldByCurrentThread()) {
lockCount.set(lockCount.get() + 1);
return true;
}
// 正常获取锁逻辑
}
公平锁保证
ZooKeeper的顺序节点天然支持公平性,先创建的节点优先获得锁。
羊群效应(Herd Effect)
多个客户端监听同一节点,节点删除时会同时唤醒所有客户端,导致性能骤降。应监听前一个节点而非根节点。
会话过期处理
当会话过期时,临时节点自动删除,但客户端可能仍认为持有锁。需通过状态检查或重连机制解决:
// 监听连接状态
client.getConnectionStateListenable().addListener((client, newState) -> {
if (newState == ConnectionState.LOST) {
// 重置锁状态
resetLockStatus();
}
});
使用成熟框架
避免手写底层逻辑,推荐使用Apache Curator框架:
// Curator的可重入锁示例
InterProcessMutex lock = new InterProcessMutex(client, "/distributed-lock");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
if (lock.isAcquiredInThisProcess()) {
lock.release();
}
}
监控与告警
监控ZooKeeper集群状态、锁等待时间、竞争激烈程度等指标,及时发现性能瓶颈。