Java中的分布式锁:Redisson/Curator与ZooKeeper/Redis的深度实践

Java 分布式锁:Redisson/Curator 与 ZooKeeper/Redis 的深度实践

大家好,今天我们来深入探讨 Java 分布式锁,特别是结合 Redisson/Curator 这两个优秀的客户端,以及 ZooKeeper/Redis 这两个主流的分布式协调服务/缓存中间件,进行实践讲解。分布式锁是解决分布式系统中并发控制的关键技术,能够保证在多个节点同时访问共享资源时,只有一个节点能够获得锁,从而避免数据不一致等问题。

为什么要用分布式锁?

在单体应用中,我们可以简单地使用 Java 自带的 synchronized 关键字或者 ReentrantLock 来实现锁机制。但在分布式环境中,这些 JVM 级别的锁只能保证单个 JVM 实例内的线程安全,无法解决多个 JVM 实例之间的并发问题。

考虑一个电商场景,多个服务器同时接收到同一商品的购买请求。如果库存管理没有做并发控制,可能会出现超卖现象,导致用户体验下降,甚至引发法律纠纷。

单机锁的局限性:

特性 单机锁 (synchronized, ReentrantLock) 分布式锁 (Redisson/Curator + ZK/Redis)
适用范围 单个 JVM 实例 多个 JVM 实例
锁的实现 基于 JVM 内存 基于分布式协调服务/缓存中间件
锁的释放 JVM 负责 需要手动释放或者依赖超时机制
可靠性 低 (JVM 崩溃锁丢失) 高 (依赖 ZK/Redis 的高可用特性)

分布式锁的实现方式

实现分布式锁有很多种方式,常见的有:

  1. 基于数据库: 利用数据库的唯一索引或者悲观锁来实现。性能较差,不推荐在高并发场景下使用。
  2. 基于 ZooKeeper: 利用 ZooKeeper 的临时节点特性来实现。可靠性高,但性能相对较低,适合对一致性要求高的场景。
  3. 基于 Redis: 利用 Redis 的 SETNX 命令和过期时间来实现。性能高,但可靠性稍差,适合对性能要求高的场景。

今天我们主要关注基于 ZooKeeper 和 Redis 的两种实现方式,并结合 Redisson 和 Curator 客户端进行实践。

基于 ZooKeeper 的分布式锁 (Curator)

ZooKeeper 是一个高可用的分布式协调服务,它提供了数据一致性保证,可以用来实现分布式锁。 Curator 是 Apache ZooKeeper 的 Java 客户端库,它提供了更高层次的 API,简化了 ZooKeeper 的使用。

原理:

  1. 客户端尝试创建一个临时顺序节点,例如 /locks/mylock-0000000001
  2. 如果创建成功,则该客户端获得锁。
  3. 如果创建失败,则监听 /locks/mylock 节点下比自己小的最小节点,等待其删除事件。
  4. 当监听到删除事件后,再次尝试创建节点,重复上述步骤。
  5. 释放锁时,删除自己创建的临时节点。

代码示例 (Curator):

首先添加 Curator 依赖:

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-client</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-test</artifactId>
    <version>5.2.1</version>
    <scope>test</scope>
</dependency>
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class CuratorLockExample {

    private static final String ZK_ADDRESS = "127.0.0.1:2181";
    private static final String LOCK_PATH = "/mylock";

    public static void main(String[] args) throws Exception {
        // 创建 CuratorFramework 客户端
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                ZK_ADDRESS,
                new ExponentialBackoffRetry(1000, 3) // 重试策略
        );

        client.start();

        // 创建 InterProcessMutex 锁对象
        InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH);

        try {
            // 尝试获取锁,最多等待 10 秒
            if (lock.acquire(10, java.util.concurrent.TimeUnit.SECONDS)) {
                try {
                    // 模拟业务逻辑
                    System.out.println(Thread.currentThread().getName() + " acquired lock, executing critical section...");
                    Thread.sleep(5000); // 模拟耗时操作
                } finally {
                    // 释放锁
                    lock.release();
                    System.out.println(Thread.currentThread().getName() + " released lock.");
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " failed to acquire lock.");
            }
        } finally {
            // 关闭 CuratorFramework 客户端
            client.close();
        }
    }
}

代码解释:

  • CuratorFrameworkFactory.newClient(): 创建 Curator 客户端,需要指定 ZooKeeper 的地址和重试策略。
  • ExponentialBackoffRetry: 指数退避重试策略,用于在连接失败时进行重试。
  • InterProcessMutex: Curator 提供的可重入互斥锁。
  • lock.acquire(): 尝试获取锁,可以指定等待时间。
  • lock.release(): 释放锁。

优点:

  • 可靠性高: 依赖 ZooKeeper 的数据一致性保证,即使客户端崩溃,锁也会自动释放。
  • 可重入性: Curator 提供的 InterProcessMutex 是可重入锁,允许同一个线程多次获取锁。
  • 公平锁: 可以通过创建顺序节点来实现公平锁,保证锁的获取顺序。

缺点:

  • 性能相对较低: 每次获取和释放锁都需要与 ZooKeeper 进行交互,增加了网络开销。
  • 复杂性较高: 需要理解 ZooKeeper 的基本概念和 API。

基于 Redis 的分布式锁 (Redisson)

Redis 是一个高性能的键值存储数据库,它提供了丰富的命令和数据结构,可以用来实现分布式锁。 Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid),它提供了易于使用和全面的 Redis 客户端,包括分布式锁、分布式集合、分布式对象等。

原理:

  1. 使用 SETNX key value 命令尝试设置键值对。如果键不存在,则设置成功,客户端获得锁。
  2. 设置键的过期时间,防止客户端崩溃导致锁无法释放,形成死锁。
  3. 如果设置失败,则循环尝试获取锁。
  4. 释放锁时,删除键。

代码示例 (Redisson):

首先添加 Redisson 依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.23.4</version>
</dependency>
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;

public class RedissonLockExample {

    private static final String REDIS_ADDRESS = "redis://127.0.0.1:6379";
    private static final String LOCK_NAME = "mylock";

    public static void main(String[] args) throws InterruptedException {
        // 创建 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress(REDIS_ADDRESS);
        Redisson redisson = (Redisson) Redisson.create(config);

        // 获取 RLock 对象
        RLock lock = redisson.getLock(LOCK_NAME);

        try {
            // 尝试获取锁,最多等待 10 秒,锁的有效期为 30 秒
            boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);

            if (isLocked) {
                try {
                    // 模拟业务逻辑
                    System.out.println(Thread.currentThread().getName() + " acquired lock, executing critical section...");
                    Thread.sleep(5000); // 模拟耗时操作
                } finally {
                    // 释放锁
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + " released lock.");
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " failed to acquire lock.");
            }
        } finally {
            // 关闭 Redisson 客户端
            redisson.shutdown();
        }
    }
}

代码解释:

  • Redisson.create(): 创建 Redisson 客户端,需要指定 Redis 的地址。
  • redisson.getLock(): 获取 RLock 对象,需要指定锁的名称。
  • lock.tryLock(): 尝试获取锁,可以指定等待时间、锁的有效期和时间单位。
  • lock.unlock(): 释放锁。
  • redisson.shutdown(): 关闭 Redisson 客户端。

优点:

  • 性能高: 基于 Redis 的内存操作,性能非常高。
  • 易于使用: Redisson 提供了简单易用的 API。
  • 丰富的功能: Redisson 提供了多种分布式锁的实现,包括可重入锁、公平锁、读写锁等。
  • 自动续期: Redisson 默认提供 Watchdog 自动续期机制,防止锁过期。

缺点:

  • 可靠性相对较低: 如果 Redis 发生故障,可能会导致锁丢失。
  • 需要考虑锁的续期问题: 如果业务逻辑执行时间超过锁的有效期,需要考虑锁的续期问题。

关于 Redis 分布式锁的续期问题

Redisson 默认会启动一个 Watchdog 线程,每隔一段时间(默认是 lockWatchdogTimeout / 3,即默认 10 秒)检查锁是否还存在,如果存在,则会延长锁的有效期。这样可以防止业务逻辑执行时间超过锁的有效期,导致锁被自动释放。

避免 Redis 分布式锁误删问题

在释放锁的时候,需要判断当前线程是否持有锁,只有持有锁的线程才能释放锁。 Redisson 已经处理了这个问题,它在释放锁的时候会检查当前线程是否持有锁,只有持有锁的线程才能释放锁,避免了误删其他线程的锁。

Curator 和 Redisson 的选择

选择 Curator 还是 Redisson,取决于具体的应用场景和需求。

特性 Curator (ZooKeeper) Redisson (Redis)
可靠性 较高
性能 较低
复杂性 较高 较低
功能 基础锁 丰富的锁类型和功能
使用场景 对一致性要求高的场景 对性能要求高的场景
维护成本 较高 较低
锁的自动释放机制 临时节点自动删除 超时机制, Watchdog 续期
  • 如果对一致性要求非常高,并且可以容忍一定的性能损耗,那么可以选择 Curator。 例如,金融系统、交易系统等。
  • 如果对性能要求非常高,并且可以接受一定的锁丢失风险,那么可以选择 Redisson。 例如,秒杀系统、抢购系统等。
  • 如果需要更丰富的锁类型和功能,例如读写锁、公平锁等,那么可以选择 Redisson。

分布式锁的最佳实践

  • 选择合适的锁实现: 根据具体的应用场景和需求,选择合适的锁实现。
  • 设置合理的锁过期时间: 避免死锁,但也要避免锁提前过期。
  • 考虑锁的续期问题: 如果业务逻辑执行时间超过锁的有效期,需要考虑锁的续期问题。
  • 避免误删锁: 确保只有持有锁的线程才能释放锁。
  • 监控锁的状态: 监控锁的获取和释放情况,及时发现和解决问题。
  • 测试锁的可靠性: 在生产环境上线前,需要对锁进行充分的测试,确保其可靠性。
  • 幂等性处理: 即使使用分布式锁,也要保证业务逻辑的幂等性,避免重复执行。因为锁有可能失效,导致并发操作。

总结

分布式锁是解决分布式系统中并发控制的关键技术。 基于 ZooKeeper 的 Curator 提供了高可靠的锁实现,适合对一致性要求高的场景。 基于 Redis 的 Redisson 提供了高性能的锁实现,适合对性能要求高的场景。 在选择分布式锁的实现方式时,需要根据具体的应用场景和需求进行权衡。 充分理解两种锁的原理和优缺点,能够帮助我们做出更明智的选择,构建更健壮的分布式系统。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注