当前位置:首页 > 问答 > 正文

分布式锁 Curator 源码解析:ZooKeeper 可重入锁的多次加锁与释放机制

分布式锁 | Curator 源码解析:ZooKeeper 可重入锁的多次加锁与释放机制

场景引入:电商秒杀中的锁困境

"老王,咱们的秒杀系统又出问题了!"测试组的小张急匆匆地跑过来,"刚才模拟测试时,有用户连续点击秒杀按钮,系统居然给同一个用户发了三件商品!"

作为团队的技术骨干,老王皱起了眉头,这已经是本月第三次出现超卖问题了,他们之前使用Redis实现的分布式锁在高峰期经常出现锁失效的情况,而且不支持可重入特性——当同一个线程需要多次获取锁时就会死锁。

"我们需要一个更可靠的方案,"老王心想,"ZooKeeper的可重入锁或许能解决这个问题..."

可重入锁:分布式环境下的"递归锁"

在单机环境下,Java的synchronized和ReentrantLock都支持可重入特性——同一个线程可以多次获取同一个锁,但在分布式系统中,实现这一特性就复杂得多。

Curator是Apache开源的ZooKeeper客户端,它提供的InterProcessMutex不仅实现了分布式锁,还支持可重入特性,今天我们就深入它的源码,看看它是如何实现多次加锁与释放机制的。

加锁流程解析:从API到底层ZK节点

锁的初始化

首先看看创建锁实例的代码:

public InterProcessMutex(CuratorFramework client, String path) {
    this(client, path, new StandardLockInternalsDriver());
}

这里需要传入Curator客户端实例和一个路径,/locks/order_lock",这个路径将成为ZooKeeper上的节点。

分布式锁 Curator 源码解析:ZooKeeper 可重入锁的多次加锁与释放机制

首次加锁:创建临时顺序节点

当我们第一次调用acquire()方法时:

public void acquire() throws Exception {
    if (!internalLock(-1, null)) {
        throw new IOException("Lost connection while trying to acquire lock: " + basePath);
    }
}

真正的魔法发生在internalLock方法中,简化后的核心逻辑:

  1. 在ZK上创建临时顺序节点,如"/locks/order_lock/_c_1234567890"
  2. 尝试获取锁:判断自己是否是当前最小序号的节点
  3. 如果不是最小,则监听前一个节点
  4. 阻塞等待直到获得锁

可重入的关键:线程持有计数

Curator使用两个关键数据结构来维护可重入状态:

private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
private static class LockData {
    final Thread owningThread;
    final String lockPath;
    final AtomicInteger lockCount = new AtomicInteger(1);
}

当线程首次获取锁时,会在threadData中记录:

LockData lockData = threadData.get(currentThread);
if (lockData != null) {
    // 重入情况
    lockData.lockCount.incrementAndGet();
    return true;
}
// 首次获取锁
lockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, lockData);

这个lockCount就是实现可重入的核心——每次重入时计数器加1,而不是重复创建ZK节点。

分布式锁 Curator 源码解析:ZooKeeper 可重入锁的多次加锁与释放机制

释放锁机制:精确的计数控制

释放锁的逻辑同样精彩:

public void release() throws Exception {
    Thread currentThread = Thread.currentThread();
    LockData lockData = threadData.get(currentThread);
    if (lockData == null) {
        throw new IllegalMonitorStateException("You do not own the lock: " + basePath);
    }
    int newLockCount = lockData.lockCount.decrementAndGet();
    if (newLockCount > 0) {
        return; // 还有重入锁未释放
    }
    if (newLockCount < 0) {
        throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
    }
    try {
        // 真正释放ZK锁
        internals.releaseLock(lockData.lockPath);
    } finally {
        threadData.remove(currentThread);
    }
}

可以看到,只有当lockCount减到0时,才会真正删除ZK节点释放锁,这种设计确保了:

  1. 多次重入后必须对应次数的释放
  2. 不会提前释放锁导致线程安全问题
  3. 最终一定会清理ZK节点避免资源泄漏

异常处理:连接丢失与竞争条件

分布式环境下网络问题不可避免,Curator对此做了精心处理:

  1. 连接恢复:如果ZK连接临时中断,Curator会保持阻塞直到连接恢复,然后继续等待锁
  2. 竞争条件防护:所有状态变更都通过原子操作和线程安全集合保证
  3. 死锁预防:ZK的临时节点特性确保客户端断开时自动释放锁

实战示例:电商库存扣减

回到开头的秒杀问题,使用Curator可重入锁的正确姿势:

InterProcessMutex lock = new InterProcessMutex(client, "/locks/item_" + itemId);
public void deductStock(Long itemId, int num) {
    try {
        lock.acquire();
        // 重入场景:内部方法也需要加锁
        doDeduct(itemId, num);
    } finally {
        lock.release();
    }
}
private void doDeduct(Long itemId, int num) throws Exception {
    lock.acquire(); // 可重入
    try {
        // 真正的业务逻辑
        Item item = itemDao.get(itemId);
        if (item.getStock() >= num) {
            item.setStock(item.getStock() - num);
            itemDao.update(item);
        }
    } finally {
        lock.release();
    }
}

性能考量与最佳实践

虽然ZK锁很强大,但也要注意:

分布式锁 Curator 源码解析:ZooKeeper 可重入锁的多次加锁与释放机制

  1. 锁粒度:不要用太粗的锁,比如所有商品共用一个锁
  2. 超时设置:使用带超时的acquire方法避免长时间阻塞
  3. 连接管理:复用Curator客户端而不是每次创建
  4. 监控:通过ZK的四字命令监控锁节点数量

分布式锁的艺术

Curator的可重入锁实现展示了分布式系统设计的精妙之处:

  1. 利用ZK的临时顺序节点实现公平锁
  2. 通过内存计数实现高效的可重入
  3. 严谨的异常处理保证可靠性
  4. 清晰的API设计隐藏了底层复杂性

"老王,新方案上线后压力测试通过了!"小张兴奋地报告,"模拟5000并发也没有出现超卖!"

老王满意地点点头,分布式锁的选择确实是一门艺术,而Curator无疑提供了一件精良的工具,理解它的实现原理,才能更好地驾驭分布式系统的复杂性。

发表评论