Redisson可重入锁(RLock)的使用与原理
Redisson的可重入锁(RLock)是分布式环境下实现线程安全的核心组件,它基于Redis实现了Java java.util.concurrent.locks.Lock
接口,支持与本地锁相似的语义,但具备跨进程的分布式特性。
一、核心特性
- 可重入性:同一线程可多次获取同一把锁而不会死锁,通过内部计数器实现。
- 自动续期(看门狗):锁默认持有30秒,业务未执行完时自动续期,防止锁提前释放。
- 公平锁支持 :通过
getFairLock()
获取公平锁,按请求顺序分配锁。 - 异步/同步支持 :提供同步(如
lock()
)和异步(如lockAsync()
)接口。 - 联锁/红锁:支持将多个锁作为一个整体管理(如RedLock算法)。
二、使用示例
java
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonLockExample {
private static RedissonClient redisson;
static {
// 配置Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
redisson = Redisson.create(config);
}
// 1. 基础用法(推荐)
public void basicUsage() {
RLock lock = redisson.getLock("order:create:1001");
try {
// 尝试获取锁,最多等待10秒,持有锁30秒后自动释放
boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLocked) {
// 执行业务逻辑
processOrder();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 确保释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 2. 可重入性演示
public void reentrantDemo() {
RLock lock = redisson.getLock("reentrant:demo");
try {
// 第一次获取锁
lock.lock();
System.out.println("第一次获取锁成功");
// 同一线程可再次获取锁(可重入)
lock.lock();
System.out.println("第二次获取锁成功");
// 执行业务逻辑
} finally {
// 需释放锁两次
lock.unlock();
System.out.println("第一次释放锁");
lock.unlock();
System.out.println("第二次释放锁");
}
}
// 3. 公平锁用法
public void fairLockDemo() {
RLock fairLock = redisson.getFairLock("fair:lock");
try {
fairLock.lock();
// 按请求顺序获取锁
processWithFairness();
} finally {
fairLock.unlock();
}
}
// 4. 异步锁用法(高并发场景)
public CompletableFuture<Void> asyncLockDemo() {
RLock lock = redisson.getLock("async:lock");
return lock.tryLockAsync(10, 30, TimeUnit.SECONDS)
.thenAccept(acquired -> {
if (acquired) {
try {
// 异步执行逻辑
processAsync();
} finally {
lock.unlockAsync();
}
}
});
}
private void processOrder() {
// 业务逻辑
}
private void processWithFairness() {
// 公平锁保护的业务逻辑
}
private void processAsync() {
// 异步业务逻辑
}
}
三、底层实现原理
1. 数据结构
Redisson使用Redis的Hash
结构存储锁信息:
- Key :锁名称(如
order:lock:1001
) - Field:线程唯一标识(UUID:ThreadId)
- Value:重入次数计数器
2. 加锁流程
通过Lua脚本原子性执行以下逻辑:
lua
-- KEYS[1]: 锁名称
-- ARGV[1]: 锁过期时间(默认30秒)
-- ARGV[2]: 线程标识(UUID:ThreadId)
-- 1. 检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
-- 2. 不存在则创建锁,初始重入次数为1
redis.call('hset', KEYS[1], ARGV[2], 1);
-- 3. 设置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 4. 锁已存在,检查是否被当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 5. 是当前线程持有,则重入次数+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 6. 重置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 7. 锁被其他线程持有,返回剩余过期时间
return redis.call('pttl', KEYS[1]);
3. 解锁流程
同样通过Lua脚本原子性执行:
lua
-- KEYS[1]: 锁名称
-- ARGV[1]: 线程标识
-- ARGV[2]: 锁释放通知频道名称
-- 1. 检查锁是否存在且被当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return nil;
end;
-- 2. 重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
-- 3. 判断重入次数是否为0
if (counter > 0) then
-- 4. 大于0则重置过期时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
-- 5. 等于0则删除锁
redis.call('del', KEYS[1]);
-- 6. 发布锁释放通知(用于唤醒等待线程)
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
4. 看门狗机制
当未指定锁过期时间时,Redisson会启动定时任务自动续期:
- 加锁成功后,启动一个每10秒执行一次的定时任务(默认看门狗超时时间30秒)。
- 任务通过Lua脚本检查锁是否存在且被当前线程持有,若是则重置过期时间为30秒。
- 解锁时取消定时任务,避免无限续期。
四、与本地锁(ReentrantLock)的对比
特性 | ReentrantLock(本地) | RLock(分布式) |
---|---|---|
跨进程支持 | ❌ | ✅ |
自动续期 | ❌ | ✅(看门狗机制) |
Redis依赖 | ❌ | ✅ |
公平锁实现 | 基于AQS队列 | 基于Redis List队列 |
异常处理 | 需手动释放锁 | 自动续期,减少锁泄漏风险 |
性能 | 高(本地内存操作) | 低(网络通信开销) |
五、最佳实践
- 使用
tryLock
而非lock
:避免线程无限阻塞(如tryLock(10, 30, TimeUnit.SECONDS)
)。 - 确保锁释放 :始终在
finally
块中调用unlock()
,并检查isHeldByCurrentThread()
。 - 合理设置过期时间:预估业务执行时间,或依赖看门狗自动续期。
- 锁粒度最小化:只锁定关键资源(如订单ID),避免大范围锁定。
- 异常处理 :捕获
InterruptedException
并恢复中断状态。
六、注意事项
-
Redis部署模式影响:
- 单节点:存在单点故障风险;
- 主从:主节点故障时可能丢失锁信息;
- 红锁(RedLock):需向多个独立节点加锁,提升可靠性。
-
长事务慎用:长时间持有锁会降低系统吞吐量,建议异步化处理。
-
集群环境时钟同步:若依赖Redis的过期时间,需确保集群节点时钟同步。