一、加锁流程
1. 核心方法调用链
RLock lock = redisson.getLock("resource");
lock.lock(); // 阻塞式加锁
↳ lockInterruptibly()
↳ tryAcquire(-1, leaseTime, unit) // leaseTime=-1表示启用看门狗
↳ tryAcquireAsync()
↳ tryLockInnerAsync() // 执行Lua脚本
2. Lua脚本实现(关键)
// RedissonLock.tryLockInnerAsync()
"if (redis.call('exists', KEYS[1]) == 0) then " + // 锁不存在
"redis.call('hset', KEYS[1], ARGV[2], 1); " + // 创建锁(Hash结构)
"redis.call('pexpire', KEYS[1], ARGV[1]); " + // 设置过期时间
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + // 锁已存在,判断是否重入
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // 重入次数+1
"redis.call('pexpire', KEYS[1], ARGV[1]); " + // 重置过期时间
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);", // 返回剩余时间,加锁失败
Collections.singletonList(getName()), // KEYS[1]: 锁名称(如"resource")
internalLockLeaseTime, getLockName(threadId) // ARGV[1]: 过期时间;ARGV[2]: 线程标识(UUID:threadId)
3. 关键点
- 原子性:通过Lua脚本保证检查锁和创建锁的原子性。
- 数据结构:使用Redis的
Hash
存储锁信息,field
为线程标识,value
为重入次数。 - 过期时间:默认30秒(看门狗机制自动续期),防止死锁。
二、解锁流程
1. 核心方法调用链
lock.unlock();
↳ unlockAsync()
↳ unlockInnerAsync() // 执行Lua脚本
2. Lua脚本实现(关键)
// RedissonLock.unlockInnerAsync()
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + // 锁不存在或非当前线程持有
"return nil; " +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + // 重入次数-1
"if (counter > 0) then " + // 重入次数>0,继续持有锁
"redis.call('pexpire', KEYS[1], ARGV[2]); " + // 重置过期时间
"return 0; " +
"else " + // 重入次数=0,释放锁
"redis.call('del', KEYS[1]); " + // 删除锁
"redis.call('publish', KEYS[2], ARGV[1]); " + // 发布锁释放消息(通知等待线程)
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getName(), getChannelName()), // KEYS[1]: 锁名称;KEYS[2]: 发布订阅通道
LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId) // ARGV[3]: 线程标识
3. 关键点
- 安全释放:仅锁持有者(
UUID:threadId
匹配)可释放锁。 - 发布订阅:锁释放时通过Redis的
PUBLISH
通知等待线程。 - 重入处理:通过
hincrby -1
递减重入次数,确保正确释放。
三、锁续时(看门狗机制)
1. 触发条件
- 当使用无参
lock()
方法时(即未指定leaseTime
),默认启用看门狗。 - 看门狗默认每10秒(
internalLockLeaseTime / 3
)续期一次,将锁过期时间重置为30秒。
2. 核心源码
// RedissonLock.scheduleExpirationRenewal()
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
entry.addThreadId(threadId);
// 创建定时任务
Timeout task = commandExecutor.getConnectionManager().newTimeout(timeout -> {
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// 续期成功,递归调用
scheduleExpirationRenewal(threadId);
}
});
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
entry.setTimeout(task);
}
3. 续期Lua脚本
// RedissonLock.renewExpirationAsync()
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + // 锁存在且为当前线程持有
"redis.call('pexpire', KEYS[1], ARGV[1]); " + // 重置过期时间
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId)
4. 关键点
- 自动续期:通过Netty的
Timeout
实现定时任务。 - 避免死锁:若业务执行时间超长,看门狗会持续续期,直到业务完成或线程崩溃。
四、重入锁实现
1. 数据结构
使用Redis的Hash
存储锁信息:
- Key:锁名称(如
"resource"
)。 - Field:线程标识(
UUID:threadId
)。 - Value:重入次数(初始为1,每次重入+1)。
2. 加锁时的重入逻辑
// Lua脚本片段
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + // 锁已存在,判断是否重入
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // 重入次数+1
"redis.call('pexpire', KEYS[1], ARGV[1]); " + // 重置过期时间
"return nil; " + // 返回nil表示加锁成功(重入)
"end; "
3. 解锁时的重入逻辑
// Lua脚本片段
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + // 重入次数-1
"if (counter > 0) then " + // 重入次数>0,继续持有锁
"redis.call('pexpire', KEYS[1], ARGV[2]); " + // 重置过期时间
"return 0; " + // 返回0表示锁未释放
"else " + // 重入次数=0,释放锁
"redis.call('del', KEYS[1]); " + // 删除锁
"return 1; " + // 返回1表示锁已释放
"end; "
4. 关键点
- 线程安全:通过
UUID:threadId
确保同一线程可重入。 - 原子计数:使用
hincrby
保证计数操作的原子性。
五、lock()与tryLock()的区别
1. 核心区别对比表
特性 |
|
|
阻塞行为 |
阻塞直到获取锁 |
立即返回或在指定时间内等待 |
超时机制 |
无超时,默认启用看门狗自动续期 |
可自定义等待时间和锁持有时间 |
异常处理 |
不响应中断(抛出 |
可响应中断(通过重载方法) |
返回值 |
|
|
看门狗默认启用 |
是(无参时) |
否(需显式设置超时参数) |
典型场景 |
必须获取锁才能执行的场景 |
可重试或放弃的场景 |
2. 源码差异分析
// lock() 源码片段
public void lock() {
try {
lock(-1, null, false); // leaseTime=-1表示启用看门狗
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// tryLock() 源码片段
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// 1. 计算超时时间
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
// 2. 尝试获取锁
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) {
return true; // 获取成功
}
// 3. 超时处理逻辑(循环尝试或等待通知)
// ...
}
3. 使用场景对比
// lock() 使用示例
RLock lock = redisson.getLock("order:123");
try {
lock.lock(); // 阻塞直到获取锁
// 执行关键业务逻辑
} finally {
lock.unlock();
}
// tryLock() 使用示例
RLock lock = redisson.getLock("inventory:apple");
try {
// 尝试在5秒内获取锁,持有30秒
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 获取锁成功,执行操作
} else {
// 获取锁失败,执行降级逻辑
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
六、总结
Redisson分布式锁的核心优势:
- 原子性:通过Lua脚本确保操作的原子性。
- 可重入:基于
Hash
结构实现线程级别的重入计数。 - 高可用:通过看门狗机制避免锁过期导致的数据不一致。
- 高性能:基于Netty的异步通信模型。
- 安全释放:通过
UUID:threadId
确保锁只能被持有者释放。
最佳实践建议:
- 优先使用
tryLock()
:避免长时间阻塞,提高系统吞吐量。 - 明确锁持有时间:根据业务场景合理设置
leaseTime
,避免过度依赖看门狗。 - 异常处理:使用带超时参数的
tryLock()
,并处理中断异常。