通过lock()获取分布式锁
调用lock()方法(获取锁没有超时时间)
关键流程
1:调用tryAcquire()方法,返回ttl;(通过目录跳转到tryAcquire())
2:若是ttl为null,说明获取锁成功,lock()方法执行结束;
3:若是null不为空,ttl则是当前持有锁线程剩余的持有时间,本线程等待被唤醒或者等待ttl时间过去后苏醒尝试获取锁;
3.1:通过subscribe()订阅锁对应的频道,获取对应的entry;
3.2:进入默认while(true)方法中,先执行一次tryAcquire()方法(在获取entry的过程中锁可能被释放),然后执行entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);作用是阻塞等待唤醒,唤醒的情况有两种:1是锁被释放了,redis通过public通知订阅频道;2是阻塞时间到了ttl,主动苏醒;
3.3:苏醒的线程重新进入循环体,重复执行3.2,知道获取锁
3.4:执行this.unsubscribe(entry, threadId),取消订阅
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
if (ttl != null) {
CompletableFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.pubSub.timeout(future);
RedissonLockEntry entry;
if (interruptibly) {
entry = (RedissonLockEntry)this.commandExecutor.getInterrupted(future);
} else {
entry = (RedissonLockEntry)this.commandExecutor.get(future);
}
try {
while(true) {
ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
if (ttl == null) {
return;
}
if (ttl >= 0L) {
try {
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException var14) {
InterruptedException e = var14;
if (interruptibly) {
throw e;
}
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else if (interruptibly) {
entry.getLatch().acquire();
} else {
entry.getLatch().acquireUninterruptibly();
}
}
} finally {
this.unsubscribe(entry, threadId);
}
}
}
tryAcquire()
获取tryAcquireAsync()返回的Future,通过get()方法等待Future执行完成,获取返回的ttl;(通过目录跳转到tryAcquireAsync())
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
tryAcquireAsync()
关键步骤:
1:调用tryLockInnerAsync(),通过lua脚本执行redis命令获取锁,若是leaseTime有传入值,便用传入值,没有则用默认值this.internalLockLeaseTime(默认为30秒)(目录跳转到 tryLockInnerAsync)
2:当tryLockInnerAsync()返回ttl后,ttl为null时,说明当前线程成功获取了锁(可能是首次获取,或同一线程重入),此时需要确定锁的租期如何管理。 若用户在获取锁时明确指定了租期(如lock(5, TimeUnit.SECONDS)),则将 Redisson 内部维护的internalLockLeaseTime(锁的默认租期)更新为用户指定的租期(转换为毫秒)。 此时不启动看门狗,因为用户已明确锁的生存时间,锁会在leaseTime到期后自动释放,无需自动续约。 若用户未指定租期(如直接调用lock()),则触发scheduleExpirationRenewal(threadId)启动看门狗机制。此时锁的默认租期为internalLockLeaseTime(默认 30 秒),看门狗会每 10 秒自动延长锁的过期时间,确保锁的生存时间与任务执行时间匹配(直到线程主动释放锁)。(目录跳转到scheduleExpirationRenewal())
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture ttlRemainingFuture;
if (leaseTime > 0L) {
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
if (ttlRemaining == null) {
if (leaseTime > 0L) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
this.scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper(f);
}
tryLockInnerAsync(加锁lua脚本解析)
KEY[1]:lock_name , ARGV[1]:锁持有时间 ,ARGV[2]: ThreadId
1:if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;
说明: 若是当前锁不存在,或者锁存在,而且持有线程是本线程(锁重入状态),调用'hincrby'指令为当前锁的重入次数加一(该命令在锁不存在的时候会创建锁),并且刷新持有时间
2: return redis.call('pttl', KEYS[1]);
说明: 若是锁被其他线程获取,则返回单签锁剩余的持有时间
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command,
"if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1))
then redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;
return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}
scheduleExpirationRenewal()
1:若是oidEntry != null 说明当前加锁状态是重入锁,实现看门狗机制的定时器已经存在
2:当前线程第一次获取锁,执行this.renewExpiration()创建实现看门狗机制的定时器;(目录跳转方法renewExpiration())
protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
try {
this.renewExpiration();
} finally {
if (Thread.currentThread().isInterrupted()) {
this.cancelExpirationRenewal(threadId);
}
}
}
}
renewExpiration()
关键点: 1:ee是从全局映射EXPIRATION_RENEWAL_MAP中获取的ExpirationEntry实例。若ee == null:说明锁已被释放(或从未被持有),无需创建续约任务,直接退出。 若ee != null:说明锁仍被持有,需要创建定时续约任务。
依旧持有锁的情况下继续执行下面的点:
2: Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {...},this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS),创建一个定时task,时间是默认持有时间的三分之一,也就是这是一个在10秒后执行内部任务的定时器;
3:定时任务:1):获取ent,在10秒内,锁可能已被释放(EXPIRATION_RENEWAL_MAP中对应的条目被移除),或ExpirationEntry的状态已发生变化(如线程释放锁后threadIds为空)。因此需要再次获取ent,确保基于最新的锁状态执行续约。2):若是锁没有释放,执行renewExpirationAsync(threadId)方法为当前锁进行续约。3)若是抛出异常,日志打印错误信息,同时移除这个entry;若是加锁成功,循环调用renewExpiration()方法,重新构建一个10秒的定时器,由此实现了自动续约的看门狗机制;若是加锁失败,说明当前线程已经释放锁,执行cancelExpirationRenewal((Long)null);释放定时器。(renewExpirationAsync目录跳转)
private void renewExpiration() {...},
ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = (ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
RedissonBaseLock.log.error("Can't update lock {} expiration", RedissonBaseLock.this.getRawName(), e);
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
} else {
if (res) {
RedissonBaseLock.this.renewExpiration();
} else {
RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
renewExpirationAsync()
lua脚本解析: 若是当前锁存在,而且持有的线程是本身,则使用命令'pexpire'重新设置持有时间(30s)并且返回1,若不是则返回0
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end;
return 0;",
Collections.singletonList(this.getRawName()), this.internalLockLeaseTime, this.getLockName(threadId));
}
调用tryLock()方法(获取锁有超时时间)
关键点: 1:初始化参数并尝试获取锁,ttl == null:当前线程成功获取到锁(首次获取或重入),直接返回true。 ttl != null:锁被其他线程持有,ttl为锁的剩余过期时间(单位:毫秒),需进入等待逻辑。(尝试获取锁的方法tryAcquire()与lock()中的一样,详细实现看lock()中的tryAcquire()目录)
2:若首次尝试获取锁失败,且已消耗完所有等待时间(time <= 0),则直接返回false,否则进入循环常识的逻辑
3:subscribe(threadId):为当前线程订阅锁释放的通知频道(如redisson_lock__channel:{lockName}),返回的subscribeFuture会在订阅完成后得到RedissonLockEntry(封装订阅状态和阻塞工具)。
4:subscribeFuture.get(time, TimeUnit.MILLISECONDS)阻塞等待订阅完成,订阅操作本身也有超时限制(使用剩余的time),若订阅超时或失败,直接返回false,并清理订阅资源。
5:订阅成功后,进入循环,在剩余等待时间内反复尝试获取锁。1)每次循环先尝试获取锁(tryAcquire),若成功直接返回。2)若失败,根据锁的剩余时间(ttl)和剩余等待时间,决定线程阻塞的时长(通过latch.tryAcquire):若ttl更小(锁即将释放),则阻塞ttl毫秒(锁释放后会被通知唤醒,减少无效等待)。若剩余等待时间更小,则阻塞剩余时间(避免超过总等待时间)。3)阻塞结束后,更新剩余等待时间,继续循环,直到超时
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) {
return true;
} else {
time -= System.currentTimeMillis() - current;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
return false;
} else {
current = System.currentTimeMillis();
CompletableFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
try {
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
} catch (TimeoutException var21) {
if (!subscribeFuture.completeExceptionally(new RedisTimeoutException("Unable to acquire subscription lock after " + time + "ms. Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
this.unsubscribe(res, threadId);
}
});
}
this.acquireFailed(waitTime, unit, threadId);
return false;
} catch (ExecutionException var22) {
this.acquireFailed(waitTime, unit, threadId);
return false;
}
try {
time -= System.currentTimeMillis() - current;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
boolean var24 = false;
return var24;
} else {
boolean var16;
do {
long currentTime = System.currentTimeMillis();
ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) {
var16 = true;
return var16;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
var16 = false;
return var16;
}
currentTime = System.currentTimeMillis();
if (ttl >= 0L && ttl < time) {
((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
} while(time > 0L);
this.acquireFailed(waitTime, unit, threadId);
var16 = false;
return var16;
}
} finally {
this.unsubscribe((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture), threadId);
}
}
}
}
unlock()
关键方法:unlockInnerAsync
LUA脚本说明: 1:若是锁不存在或者持有锁的线程不是本线程返回nil; 2:若是线程持有锁,线程重复次数减一,并且返回当前重入次数counter 3:若counter>0,线程不释放锁,刷新锁的过期时间 4:否则说明,当前线程需要释放锁,同时通过命令publish通知订阅条目锁释放
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0;
else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end;
return nil;",
Arrays.asList(this.getRawName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)});
}