使用Java语言实现基于Zookeeper实现分布式锁

前期, 我们介绍了什么是分布式锁及分布式锁应用场景,并分享了基于Redis方案实现的分布式锁, 今天我们基于Zookeeper方案来实现分布式锁的应用。

一. 方案概述

1.1. 实现原理

  1. 临时顺序节点:每个客户端请求锁时,在ZooKeeper的指定节点下创建一个临时顺序节点。
  2. 锁竞争机制
    • 客户端创建节点后,获取所有子节点列表并排序
    • 如果自己创建的节点是序号最小的节点,则获得锁
    • 否则,监听前一个节点的删除事件,进入等待状态
  3. 自动释放:客户端会话结束(如崩溃)时,临时节点自动删除,后续节点自动获得锁

使用Java语言实现基于Zookeeper实现分布式锁_第1张图片

1.2 核心优势

  • 强一致性:基于ZooKeeper的原子广播协议(ZAB),保证锁的可靠性
  • 高可用:集群模式下部分节点故障不影响锁服务
  • 避免死锁:临时节点特性确保客户端崩溃后锁自动释放
  • 公平锁:通过顺序节点天然实现FIFO的公平性

1.3 使用场景

  • 对一致性要求极高的场景(如分布式事务协调)
  • 避免单点故障的高可用场景
  • 需要公平锁特性的场景

二. 代码实现

以下是基于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 等成熟框架,避免重复造轮子,同时结合业务场景调整参数(如会话超时、重试策略),以达到最佳效果。

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();
        }
    }
    
  • 异常恢复
    捕获并处理KeeperExceptionInterruptedException等异常,确保锁状态一致性:

    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集群状态、锁等待时间、竞争激烈程度等指标,及时发现性能瓶颈。

你可能感兴趣的:(微服务架构,Java应用,分布式,java-zookeeper,java)