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 的高可用特性) |
分布式锁的实现方式
实现分布式锁有很多种方式,常见的有:
- 基于数据库: 利用数据库的唯一索引或者悲观锁来实现。性能较差,不推荐在高并发场景下使用。
- 基于 ZooKeeper: 利用 ZooKeeper 的临时节点特性来实现。可靠性高,但性能相对较低,适合对一致性要求高的场景。
- 基于 Redis: 利用 Redis 的
SETNX命令和过期时间来实现。性能高,但可靠性稍差,适合对性能要求高的场景。
今天我们主要关注基于 ZooKeeper 和 Redis 的两种实现方式,并结合 Redisson 和 Curator 客户端进行实践。
基于 ZooKeeper 的分布式锁 (Curator)
ZooKeeper 是一个高可用的分布式协调服务,它提供了数据一致性保证,可以用来实现分布式锁。 Curator 是 Apache ZooKeeper 的 Java 客户端库,它提供了更高层次的 API,简化了 ZooKeeper 的使用。
原理:
- 客户端尝试创建一个临时顺序节点,例如
/locks/mylock-0000000001。 - 如果创建成功,则该客户端获得锁。
- 如果创建失败,则监听
/locks/mylock节点下比自己小的最小节点,等待其删除事件。 - 当监听到删除事件后,再次尝试创建节点,重复上述步骤。
- 释放锁时,删除自己创建的临时节点。
代码示例 (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 客户端,包括分布式锁、分布式集合、分布式对象等。
原理:
- 使用
SETNX key value命令尝试设置键值对。如果键不存在,则设置成功,客户端获得锁。 - 设置键的过期时间,防止客户端崩溃导致锁无法释放,形成死锁。
- 如果设置失败,则循环尝试获取锁。
- 释放锁时,删除键。
代码示例 (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 提供了高性能的锁实现,适合对性能要求高的场景。 在选择分布式锁的实现方式时,需要根据具体的应用场景和需求进行权衡。 充分理解两种锁的原理和优缺点,能够帮助我们做出更明智的选择,构建更健壮的分布式系统。