JAVA Redis 分布式锁失效?基于 Redisson 实现高可靠锁机制的完整方案
大家好,今天我们来聊聊 Redis 分布式锁,以及如何使用 Redisson 构建高可靠的锁机制,避免常见的锁失效问题。
1. Redis 分布式锁的常见问题
Redis 作为高性能的缓存和数据存储,经常被用于实现分布式锁。然而,简单地使用 SETNX 和 EXPIRE 命令来实现锁,存在很多潜在的问题,例如:
- 锁的误删除: 客户端 A 获取锁后,由于某些原因(例如 GC 停顿)导致锁的过期时间到了,Redis 自动释放了锁。此时,客户端 B 获得了锁。随后,客户端 A 恢复,尝试删除锁,但实际上删除的是客户端 B 的锁,导致锁的安全性被破坏。
- 死锁: 客户端获取锁后,因为程序 Bug 或其他原因,没有释放锁,导致其他客户端无法获取锁,从而造成死锁。
- 锁续期问题: 如果客户端持有锁的时间超过了锁的过期时间,但任务尚未完成,锁会被自动释放,导致并发问题。
2. 为什么选择 Redisson
Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了常见的 Redis 数据结构(如 Set, Map, List 等),还提供了许多高级特性,包括分布式锁、分布式集合、分布式对象、消息队列等。
Redisson 在实现分布式锁方面,做了很多优化,解决了上面提到的常见问题,并提供了更丰富的功能:
- 自动续期(Watchdog): Redisson 提供了 Watchdog 机制,当客户端持有锁的时间超过锁过期时间的三分之一时,会自动给锁续期,避免锁的自动释放。
- 可重入锁: 同一个线程可以多次获取同一个锁。
- 公平锁: 按照请求锁的顺序来获取锁,避免饥饿。
- 读写锁: 支持读写分离,提高并发性能。
- 联锁(MultiLock): 可以同时锁定多个 Redis 锁。
- 红锁(RedLock): 基于多个 Redis 实例实现锁,提高锁的可靠性。
3. Redisson 分布式锁的核心原理
Redisson 分布式锁的核心原理主要依赖于 Lua 脚本和 Redis 的原子性操作。下面是 Redisson 实现锁的关键 Lua 脚本之一(用于获取锁):
if (redis.call("exists", KEYS[1]) == 0) then
redis.call("hset", KEYS[1], ARGV[1], 1);
redis.call("pexpire", KEYS[1], ARGV[2]);
return nil;
end;
if (redis.call("hexists", KEYS[1], ARGV[1]) == 1) then
redis.call("hincrby", KEYS[1], ARGV[1], 1);
redis.call("pexpire", KEYS[1], ARGV[2]);
return nil;
end;
return redis.call("pttl", KEYS[1]);
这个 Lua 脚本的逻辑如下:
- 检查锁是否存在:
redis.call("exists", KEYS[1]) == 0检查锁的 Key 是否存在。如果不存在,说明锁当前没有被占用。 - 创建锁:
redis.call("hset", KEYS[1], ARGV[1], 1)使用hset命令创建一个哈希表,其中 Key 是锁的 Key,Field 是线程的唯一标识 (UUID + Thread ID),Value 是 1 (表示锁的计数)。 - 设置过期时间:
redis.call("pexpire", KEYS[1], ARGV[2])设置锁的过期时间,单位是毫秒。 - 可重入:
redis.call("hexists", KEYS[1], ARGV[1]) == 1如果锁已经存在,并且当前线程已经持有锁(即哈希表中存在对应的 Field),则增加锁的计数。 - 返回剩余时间: 如果锁已经被其他线程持有,则返回锁的剩余时间。
关键点:
- 原子性: Lua 脚本在 Redis 中是原子性执行的,保证了获取锁操作的原子性。
- 可重入性: 使用哈希表存储线程标识和锁的计数,支持可重入锁。
- 过期时间: 设置锁的过期时间,防止死锁。
- Watchdog: Redisson 会在后台启动一个 Watchdog 线程,定期检查锁的剩余时间,如果剩余时间小于过期时间的三分之一,则自动续期。
4. 基于 Redisson 实现高可靠锁机制的完整方案
接下来,我们通过一个完整的示例,演示如何使用 Redisson 实现高可靠的分布式锁。
4.1 引入 Redisson 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.8</version>
</dependency>
4.2 配置 Redisson 客户端
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonConfig {
private static RedissonClient redisson;
public static RedissonClient getRedissonClient() {
if (redisson == null) {
synchronized (RedissonConfig.class) {
if (redisson == null) {
Config config = new Config();
// 单节点模式
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 集群模式
// config.useClusterServers().addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001", "redis://127.0.0.1:7002");
redisson = Redisson.create(config);
}
}
}
return redisson;
}
public static void shutdown() {
if (redisson != null) {
redisson.shutdown();
}
}
public static void main(String[] args) {
RedissonClient redissonClient = getRedissonClient();
System.out.println("Redisson is connected: " + redissonClient.isShuttingDown());
shutdown();
}
}
4.3 使用 Redisson 实现分布式锁
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
public class DistributedLockExample {
private static final String LOCK_KEY = "myLock";
private static final int LOCK_LEASE_TIME = 10; // 锁的过期时间,单位:秒
public void processTask() throws InterruptedException {
RedissonClient redisson = RedissonConfig.getRedissonClient();
RLock lock = redisson.getLock(LOCK_KEY);
try {
// 尝试获取锁,最多等待 10 秒,锁的过期时间为 10 秒
boolean isLocked = lock.tryLock(10, LOCK_LEASE_TIME, TimeUnit.SECONDS);
if (isLocked) {
try {
System.out.println(Thread.currentThread().getName() + ": Acquired lock, processing task...");
// 模拟耗时操作
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + ": Task completed.");
} finally {
// 释放锁
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println(Thread.currentThread().getName() + ": Released lock.");
} else {
System.out.println(Thread.currentThread().getName() + ": Lock not held by current thread, cannot release.");
}
}
} else {
System.out.println(Thread.currentThread().getName() + ": Failed to acquire lock.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
DistributedLockExample example = new DistributedLockExample();
// 模拟多个线程竞争锁
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
example.processTask();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// 等待所有线程执行完成
Thread.sleep(20000);
RedissonConfig.shutdown();
}
}
代码解释:
- 获取 Redisson 客户端:
RedissonConfig.getRedissonClient()获取 Redisson 客户端实例。 - 获取锁对象:
redisson.getLock(LOCK_KEY)根据锁的 Key 获取RLock对象。 - 尝试获取锁:
lock.tryLock(10, LOCK_LEASE_TIME, TimeUnit.SECONDS)尝试获取锁,最多等待 10 秒,锁的过期时间为 10 秒。tryLock()方法具有非阻塞特性,可以避免线程一直等待锁。 - 处理任务: 如果成功获取到锁,则执行任务逻辑。
- 释放锁: 在
finally块中释放锁,确保即使发生异常也能释放锁。lock.isLocked() && lock.isHeldByCurrentThread()确保只有持有锁的线程才能释放锁,避免误删除其他线程的锁。
4.4 Redisson 锁的不同类型
Redisson 提供了多种类型的锁,以满足不同的业务需求。
| 锁类型 | 描述 |
|---|---|
RLock |
可重入锁,同一个线程可以多次获取同一个锁。 |
RReadWriteLock |
读写锁,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。 |
RSemaphore |
信号量,控制对共享资源的并发访问数量。 |
RCountDownLatch |
倒计时器,允许一个或多个线程等待其他线程完成操作。 |
RFairLock |
公平锁,按照请求锁的顺序来获取锁,避免饥饿。 |
RMultiLock |
联锁,可以同时锁定多个 Redis 锁。 |
RedLock |
红锁,基于多个 Redis 实例实现锁,提高锁的可靠性。 |
4.5 RedLock 的实现原理和适用场景
RedLock 是 Redisson 提供的一种更高级的分布式锁,它基于多个 Redis 实例来实现锁,提高了锁的可靠性。
RedLock 的实现原理如下:
- 客户端尝试在 N 个独立的 Redis 实例上获取锁。
- 客户端按照相同的 Key 和 Value,依次向 N 个 Redis 实例发送加锁命令。
- 如果客户端在大多数 (N/2 + 1) Redis 实例上成功获取到锁,并且获取锁的总耗时没有超过锁的有效时间,则认为加锁成功。
- 如果加锁成功,客户端需要设置锁的有效时间,防止死锁。
- 如果加锁失败,客户端需要向所有 Redis 实例发送释放锁的命令。
RedLock 的优点:
- 高可用性: 即使部分 Redis 实例发生故障,只要大多数实例可用,锁仍然可用。
- 避免脑裂: RedLock 可以避免 Redis 集群发生脑裂时,多个客户端同时获取到锁的情况。
RedLock 的缺点:
- 复杂性: RedLock 的实现比较复杂,需要维护多个 Redis 连接。
- 性能: RedLock 的性能比单实例的锁要差,因为需要向多个 Redis 实例发送请求。
- 成本: 需要部署多个 Redis 实例,增加了成本。
RedLock 的适用场景:
RedLock 适用于对锁的可靠性要求非常高的场景,例如:
- 分布式事务
- 金融系统
- 关键业务流程
RedLock 的使用示例:
import org.redisson.api.RedissonClient;
import org.redisson.api.RLock;
import org.redisson.RedissonMultiLock;
import java.util.concurrent.TimeUnit;
public class RedLockExample {
public static void main(String[] args) throws InterruptedException {
RedissonClient redisson1 = RedissonConfig.getRedissonClient(); // 假设redisson1, redisson2, redisson3 连接不同的redis实例
RedissonClient redisson2 = RedissonConfig.getRedissonClient();
RedissonClient redisson3 = RedissonConfig.getRedissonClient();
RLock lock1 = redisson1.getLock("myRedLock");
RLock lock2 = redisson2.getLock("myRedLock");
RLock lock3 = redisson3.getLock("myRedLock");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
try {
boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS); // 等待10秒,锁30秒后自动释放
if (isLocked) {
try {
System.out.println("Acquired RedLock!");
Thread.sleep(20000); // 模拟业务处理
} finally {
lock.unlock();
System.out.println("Released RedLock!");
}
} else {
System.out.println("Failed to acquire RedLock!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
RedissonConfig.shutdown();
}
}
}
5. Redisson 锁的注意事项
- 选择合适的锁类型: 根据业务需求选择合适的锁类型,例如,如果需要读写分离,可以选择
RReadWriteLock。 - 设置合理的过期时间: 锁的过期时间应该根据任务的执行时间来设置,过短会导致锁的误释放,过长会导致死锁。
- 正确释放锁: 务必在
finally块中释放锁,确保即使发生异常也能释放锁。 - 避免长时间持有锁: 尽量缩短持有锁的时间,减少锁的竞争。
- 监控锁的状态: 监控锁的获取和释放情况,及时发现潜在的问题。
6. 关于锁的可靠性总结
使用 Redisson 提供的分布式锁,尤其是 RedLock,可以显著提高锁的可靠性,避免常见的锁失效问题。Redisson 通过 Lua 脚本保证了锁操作的原子性,通过 Watchdog 机制实现了锁的自动续期,并提供了多种类型的锁以满足不同的业务需求。选择合适的锁类型,设置合理的过期时间,并正确释放锁,是构建高可靠分布式锁机制的关键。
7. 选择合适的锁,让业务更可靠
Redisson 提供了多种类型的锁,每种锁都有其特定的适用场景。理解这些锁的特性,并根据业务需求选择合适的锁,是构建高可靠分布式系统的关键。