Redisson——浅析分布式锁之加锁
1. 分享目的
分布式锁往往有很多的情况需要考虑,比如锁住的任务还没执行完就超过锁过期时间,可重入等。这里通过分析Redisson
加锁来对这些问题提供解决方案及完善需要考虑的情况。Redisson有几个加锁的方法,这里只分析经常使用的tryLock()
.
2. 源码分析
加锁tryLock()
@Override
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();
// 尝试获取锁,ttl为空表示成功获取锁,否则返回当前锁的剩余时间
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) {
return true;
}
// 计算剩余等待时间
time -= System.currentTimeMillis() - current;
if (time <= 0) {
// 获取锁失败
acquireFailed(waitTime, unit, threadId);
return false;
}
current = System.currentTimeMillis();
// 订阅解锁消息
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 剩余等待时间内 阻塞线程
// 如果返回false则表示等待时间内没有解锁,则获取锁失败
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
// 取消订阅
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
// 收到解锁消息,则多个线程竞争锁
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 间隔时间自旋
while (true) {
long currentTime = System.currentTimeMillis();
// 尝试获取锁
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
currentTime = System.currentTimeMillis();
// 锁剩余时间和剩余等待时间取最小值t,在t时间内,使用Semaphore阻塞线程
// 如在t时间内Semaphore.release(),表示已解锁,继续循环竞争锁
if (ttl >= 0 && ttl < time) {
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
}
加锁主流程
-
在
tryAcquire()
中通过netty跟redis进行通讯执行加锁的lua脚本,获取锁成功则返回,失败则获取锁剩余时间ttl; -
订阅解锁消息,避免无效的自旋竞争锁,阻塞等待解锁消息;
-
收到解锁消息后,此时会进入间隔时间自旋,即多个线程竞争锁,当获取锁失败后,根据剩余等待时间和锁剩余时间的最小值t阻塞线程,阻塞后再次进入循环竞争锁;
竞争锁tryAcquire()
/**
* 竞争锁
*/
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// 使用默认leaseTime的话这里会增加watchDog机制
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// 表示获取到锁
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
// 获取锁是否成功
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " + // 不存在key,则设置key的值value+1,并设置过期时间
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // 返回null
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + // 存在key则重入锁value+1,重置过期时间
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // 返回null
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);", // 返回剩余时间
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
-
这里通过netty发送lua脚本保证原子性,这条lua脚本的逻辑:
- 是否存在等于锁名字的key,如果不存在,则设置数据结构为Hash,并将线程ID做为其中的属性,属性值+1用来做可重入锁,设置过期时间,返回null;这里表示获取到了锁;
- 当key存在,线程ID也存在其中的属性时,则将属性值+1,表示可重入,重置过期时间,返回null;表示获取到了锁;
- 当key存在,线程ID不等于其属性时,表示没获取到锁,返回锁的剩余时间。
-
当使用默认的锁释放时间时,会添加watchdog机制
private void scheduleExpirationRenewal(long threadId) { ExpirationEntry entry = new ExpirationEntry(); ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry); if (oldEntry != null) { oldEntry.addThreadId(threadId); } else { entry.addThreadId(threadId); // 定时监听线程 renewExpiration(); } } private void renewExpiration() { ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ee == null) { return; } // 使用HashedWheelTimer定时执行任务 Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null) { return; } Long threadId = ent.getFirstThreadId(); if (threadId == null) { return; } // 调用redis对锁续约 RFuture<Boolean> future = renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { log.error("Can't update lock " + getName() + " expiration", e); return; } // redis续约成功递归 if (res) { renewExpiration(); } }); } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); } protected RFuture<Boolean> renewExpirationAsync(long threadId) { // 锁存在且对应的属性包含threadId则重置锁的过期时间 return evalWriteAsync(getName(), 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(getName()), internalLockLeaseTime, getLockName(threadId)); }
使用HashedWheelTimer在默认过期时间的三分之一(10s)后执行任务,任务内容是调用redis对锁续约,续约成功再次递归调用。
订阅解锁消息subscribe()
主要逻辑代码在PublishSubscribe.subscribe()
public RFuture<E> subscribe(String entryName, String channelName) {
AtomicReference<Runnable> listenerHolder = new AtomicReference<Runnable>();
AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName));
RPromise<E> newPromise = new RedissonPromise<E>() {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return semaphore.remove(listenerHolder.get());
}
};
// redis发布订阅监听器,当收到redis消息时执行
Runnable listener = new Runnable() {
@Override
public void run() {
E entry = entries.get(entryName);
if (entry != null) {
entry.acquire();
// 释放加锁时subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS)
semaphore.release();
entry.getPromise().onComplete(new TransferListener<E>(newPromise));
return;
}
E value = createEntry(newPromise);
value.acquire();
E oldValue = entries.putIfAbsent(entryName, value);
if (oldValue != null) {
oldValue.acquire();
semaphore.release();
oldValue.getPromise().onComplete(new TransferListener<E>(newPromise));
return;
}
RedisPubSubListener<Object> listener = createListener(channelName, value);
service.subscribe(LongCodec.INSTANCE, channelName, semaphore, listener);
}
};
semaphore.acquire(listener);
listenerHolder.set(listener);
return newPromise;
}
该方法的主要逻辑是组装redis的订阅发布,并且定义监听器,当收到解锁消息时执行监听器内容。
间隔自旋
当收到解锁消息后,此时可能会有多个线程竞争锁,这里会进入间隔自旋中。
- 竞争锁,即调用
tryAcquire()
; - 没获取到锁,则根据锁的剩余时间和剩余等待时间之间的最小值t,在t时间内使用
Semaphore
进行线程阻塞,这里可以联系上述订阅的代码,收到解锁消息后的监听任务里,调用了Semaphore.release()
; - 阻塞后再次竞争锁。
3. 总结
讲了源码的主要流程,忽略了其中RFuture
、HashedWheelTimer
等异步多线程设计,为了对redisson分布式加锁有个整体的认识,后面会对其中用到的设计进行详细分析。
世界那么大,感谢遇见,未来可期…
欢迎同频共振的那一部分人
作者公众号:Tarzan写bug