一、加锁流程
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()
,并处理中断异常。