Redis提供的分布式锁有多个,这篇笔记主要记录redissonLock的相关内容
redissonLock的加锁思想:
一、这是加锁的最外层判断
/**
* 这里是加锁的逻辑
* leaseTime是加锁时长
*/
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 1.尝试加锁,如果加锁成功,返回的是nil,如果加锁失败,会返回当前key对应的锁失效时间
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired 如果等于null,就表示加锁成功,return即可
if (ttl == null) {
return;
}
// 2.加锁失败,订阅对应的channel:channelName = redisson_lock__channel + 加锁的key
// 这里订阅,是Redis中提供的功能,具体的细节没有看
RFuture<RedissonLockEntry> future = subscribe(threadId);
// 这里的interruptibly是调用方法的时候入参进来的,这里应该是阻塞,具体没搞懂
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
// 这里是一个死循环,除非当前线程加锁成功,直接break
try {
while (true) {
// 3.这里是进行一次加锁,如果线程加锁成功,就中断死循环,取消订阅,并返回
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
try {
// 4.这里是在依旧加锁失败的时候,进行await,这里可以点进去看下,实际调用的是semaphore的方法,初始化的信号量为0
// 也就是说,tryAcquire会阻塞等待
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
// 5.这里else的场景,暂时没有想到是哪种场景下会进入到这里,有可能返回的是加锁的key的过期时间 -1?
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 6.在finally中会取消订阅,如果是线程自己尝试加锁成功了,就会取消订阅;
// 在锁被释放的时候,会发消息,通知对应的channelName,可以进行加锁了;在订阅者监听到锁被释放的消息之后,会唤醒对应的线程去加锁
unsubscribe(future, threadId);
}
}
二、下面来看加锁的核心逻辑
// 这里是尝试加锁的代码
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
// 1.如果指定了超时时间,就进入下面这个分支执行加锁的逻辑
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 2.如果没有指定超时时间,会进入到这里,默认加锁30S
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
// 3.在加锁成功之后,会开启一个线程,在第10S的时候,进行锁续期
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
上面的代码可以看到:
三、最底层加锁的lua脚本
// 这里就是真正加锁的lua脚本了
<T> RFuture<T> tryLockInnerAsync(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 " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"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); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
Redis分布式锁,底层采用的是hash结构
key是加锁的值,也就是在调用lock()方法时,指定的key
field是根据当前加锁的线程ID生成的一个值
value是重入次数
我觉得这段代码,是分布式锁的核心所在,为什么说分布式锁会比setnx要优秀,我觉得这段代码是很关键的一部分原因,通过lua脚本实现
这段lua脚本大致的思想是这样的:
四、锁续期
续期的代码是从 org.redisson.RedissonLock#scheduleExpirationRenewal 这个方法调用过来的
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 锁续期的核心代码,就是下面这个run方法
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;
}
// 上面应该都是一些判断,最终会调用下面这个代码,这里的lua脚本就是续期的
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
// 如果续期成功,会循环调用
if (res) {
// reschedule itself
renewExpiration();
}
});
}
// internalLockLeaseTime 这个参数是加锁的时间,默认是30S,这里的意思是,会延迟 30 / 3的时间去执行run方法
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
所以:在续期的时候,会在有效期的三分之一时间时,进行续期,续期成功之后,会再次调用
下面是续期的lua脚本
protected RFuture<Boolean> renewExpirationAsync(long 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));
}
org.redisson.RedissonLock#unlock
org.redisson.RedissonLock#unlockAsync(long)
org.redisson.RedissonLock#unlockInnerAsync
// 这是解锁的lua脚本
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 1.如果key为0,就直接发布一个锁释放的消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 2.如果key + 线程id对应的value为0,表示当前线程所加的锁已经被释放,return即可
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 3.如果上面条件都不满足,就将key + threadId对应的value - 1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 3.1 如果-1之后,依旧大于0,表示重入了,重新设置超时时间即可
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
// 3.2 否则,表示当前key + threadId已经释放完毕
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
在解锁的时候,也有几种场景
1.key、field对应的value为0,表示锁已经释放,return nil即可
2.如果不等于0,就减1,如果减1之后的count依旧大于0,表示是锁重入,还有锁未释放,此时将锁实现时间重新设置为指定的阈值(如果没有指定,默认为30S)
3.如果减1之后的值为0,就del即可,然后发布消息,在对应的channel中,发布个unlock消息
这里所说的channel,就是前面加锁时,其他线程在加锁失败之后,所订阅的消息
所以,redissonLock就是这个逻辑,没有waitTime的概念,线程B加锁的时候,如果线程A已经加锁,就会循环一致等待重试