Redisson有两个类,分别是RedissonMultiLock和RedsissonLock,都可以实现加锁,但是里面的实现逻辑有些差别
RedissonMultiLock
RedissonMultiLock可以指定等待时间,也就是说,假如我指定了等待时间是2S,比如:
1.A线程来加锁,正常去执行业务逻辑
2.B线程也来加锁,此时会加锁失败,那B线程最多等待2S,如果超过了2S还没有获取到分布式锁,那B线程加锁就返回false,表示加锁失败
加锁
RedissonMultiLock和RedissonRedLock有关系
而RedissonRedLock大致的思想是:在加锁的时候,Redis必须是集群配置,加锁的时候,会依次去多台Redis机器上进行加锁,如果一半以上的锁加锁成功了,就认为是加锁成功了,这个点我没有去看代码验证,只是先看了下加锁和解锁的部分细节
org.redisson.RedissonMultiLock#tryLock(long, long, java.util.concurrent.TimeUnit)
在加锁的时候,会调用到这个方法,这里入参的意思分别是:
第一个waitTime是锁等待时间
leaseTime是锁加锁时间
TimeUnit是时间单位
我的理解是:线程A在加锁的时候,会使用leaseTime作为锁失效时间,如果没有指定,就是默认30S,然后进行锁续期;如果指定了,那就使用指定的阈值,并且不会自动化续期
线程B来加锁的时候,如果线程A已经对同一个key进行了加锁,那线程B最多等待waitTime时间,如果到了这个时间,线程B依旧没有获取到锁,那线程B加锁的方法就会返回false,表示加锁失败
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long newLeaseTime = -1;
// 1.这里是在指定了超时时间的时候,会将新的超时时间设置为锁等待时间的2倍;这里我没怎么理解为什么要这么做
if (leaseTime != -1) {
newLeaseTime = unit.toMillis(waitTime)*2;
}
long time = System.currentTimeMillis();
long remainTime = -1;
// 2.如果waitTime不等于-1,就是指定了等待时间的场景下,将等待时间转换为毫秒
if (waitTime != -1) {
remainTime = unit.toMillis(waitTime);
}
// 3.这里的计算,会用remainTime / Redis机器的数量;所以:如果我指定的等待时间是2000ms,那这里获取到的lockWaitTime就是666ms
long lockWaitTime = calcLockWaitTime(remainTime);
int failedLocksLimit = failedLocksLimit();
List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
// 4.对所有的Redis加锁机器进行遍历,然后依次去加锁,如果加锁失败的话,会进行一些列的处理;这里加锁失败的处理逻辑我还没有细看,后面再补充
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
// 4.1 如果等待时间未指定、锁超时时间未指定,那就调用下面这个方法即可
if (waitTime == -1 && leaseTime == -1) {
lockAcquired = lock.tryLock();
} else {
// 4.2 反之,就会调用入参了waitTime和leaseTime的方法
long awaitTime = Math.min(lockWaitTime, remainTime);
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (RedisResponseTimeoutException e) {
unlockInner(Arrays.asList(lock));
lockAcquired = false;
} catch (Exception e) {
lockAcquired = false;
}
if (lockAcquired) {
acquiredLocks.add(lock);
} else {
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
if (failedLocksLimit == 0) {
unlockInner(acquiredLocks);
if (waitTime == -1 && leaseTime == -1) {
return false;
}
failedLocksLimit = failedLocksLimit();
acquiredLocks.clear();
// reset iterator
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
failedLocksLimit--;
}
}
if (remainTime != -1) {
remainTime -= (System.currentTimeMillis() - time);
time = System.currentTimeMillis();
if (remainTime <= 0) {
unlockInner(acquiredLocks);
return false;
}
}
}
if (leaseTime != -1) {
List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture : futures) {
rFuture.syncUninterruptibly();
}
}
return true;
}
这里有一个点需要注意的是:不存在只指定锁超时时间,不指定等待时间的场景,因为redisson没有提供这样的接口;
但是会存在只指定了等待时间,没有指定超时时间的场景
根据上面源码的判断,如果指定了超时时间,指定了等待时间,那默认的加锁时间是等待时间的2倍,这里不明白原因
// 这是加锁的核心逻辑,这里我们不管前面传过来的加锁时间等待时间是多少,我们就认为等待时间是2S,加锁时间是5S
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// 1.这里的这个time就是当前加锁的线程如果加锁失败,最多等待的时间;获取到当前时间、当前线程ID
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
final long threadId = Thread.currentThread().getId();
// 2.这里就是尝试加锁的逻辑,和redissonLock加锁调用的是同一个方法,就不做过多注释;如果返回的ttl不为null,就表示加锁失败
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
// 3.在第一次加锁失败之后,会判断等待时间是否超过了阈值;如果超过了,就是说等待时间超过了2S,就返回false
time -= (System.currentTimeMillis() - current);
if (time <= 0) {
acquireFailed(threadId);
return false;
}
// 4.如果不超过阈值,就订阅对应的channel
current = System.currentTimeMillis();
final RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 4.1 这里是调用了await方法,跳进去,发现调用的是countDownLatch.await()方法,总之如果await失败,会进行一系列的处理
if (!await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.addListener(new FutureListener<RedissonLockEntry>() {
@Override
public void operationComplete(Future<RedissonLockEntry> future) throws Exception {
if (subscribeFuture.isSuccess()) {
unsubscribe(subscribeFuture, threadId);
}
}
});
}
acquireFailed(threadId);
return false;
}
try {
// 4.2 在await了之后,再次判断是否超过了等待时间
time -= (System.currentTimeMillis() - current);
if (time <= 0) {
acquireFailed(threadId);
return false;
}
// 5.如果依旧没有达到阈值,就进行尝试加锁,如果加锁成功,则返回true
// 如果加锁失败,在time的基础之上,再减去加锁的耗时
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= (System.currentTimeMillis() - currentTime);
if (time <= 0) {
acquireFailed(threadId);
return false;
}
// waiting for message
// 6.代码执行到这里,表示依旧没有超过等待的阈值;这里的getLatch()获取到的是semaphore的一个实现类,会调用其UNSAFE.park(false, nanos);方法
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= (System.currentTimeMillis() - currentTime);
if (time <= 0) {
acquireFailed(threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
RedissonLock
加锁
/**
* 这里是加锁的逻辑
*/
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的方法
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);
}
// get(lockAsync(leaseTime, unit));
}
redissonLock加锁的思想是这样的:
1.在加锁的时候,指定了超时时间和未指定超时时间的唯一区别是:
如果没有指定超时时间,第一次默认设置过期时间是30S,在第10S的时候,会进行一次锁续期,重新续期为30S;在第10S的时候会再次续期为30S,所以,如果业务系统一直没有执行完成,会不断的自动续期
如果指定了超时时间,就会设置超时时间为指定的阈值,比如设置5S,如果5S之后,业务系统的代码还没有执行完,Redis会自动释放锁
2.如果线程A加锁成功,就会返回null,如果此时线程B接着来加锁,就会加锁失败,加锁失败的时候,返回了线程A加的锁失效时间
3.线程B在拿到失效时间之后,会去订阅对应的key,然后再休眠指定的时间,这里的时间就是失效时间,如果还有4S失效,那就休眠4S
4.如果在这4S之内,线程A释放了锁,就会唤醒线程B去尝试加锁
5.如果线程A在4S之后没有释放锁,那线程B会尝试加一次锁,如果依旧加锁失败,就会再次休眠,休眠时间由返回的锁失效时间决定
// 这里是尝试加锁的代码
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是重入次数
这里的lua脚本,有三种场景
1.当前key未加锁,就是第一个if判断,会直接写入一个hash结构,并将value设置为1
2.当前key已经加锁,但是key、field都对应的上,就是所谓的重入,这种情况会将value + 1
3.key已经存在,且线程ID和已经加锁的线程不一样,此时返回key的失效时间
续期
续期的代码是从 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));
}
这段lua脚本的意思是:
释放锁
调用链是这样的
org.redisson.RedissonLock#unlock
org.redisson.RedissonLock#unlockAsync(long)
org.redisson.RedissonLock#unlockInnerAsync
// 这是解锁的lua脚本
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getName(), 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(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
在解锁的时候,也有几种场景
1.key、field对应的value为0,表示锁已经释放,return nil即可
2.如果不等于0,就减1,如果减1之后的count依旧大于0,表示是锁重入,还有锁未释放,此时将锁实现时间重新设置为指定的阈值(如果没有指定,默认为30S)
3.如果减1之后的值为0,就del即可,然后发布消息,在对应的channel中,发布个unlock消息
这里所说的channel,就是前面加锁时,其他线程在加锁失败之后,所订阅的消息
总结
对于两种锁,底层调用的加锁方法是同一个,都是通过lua脚本来加锁的,只是在外层的判断处理不一样
redLock实现了multiLock,redLock是需要多台Redis机器,在加锁的时候,是会遍历去多台Redis主机去加锁,如果一半以上的Redis主机加锁成功,就认为是加锁成功了
multiLock可以指定等待时间和超时时间,等待时间
等待时间是指在加锁的时候,如果已经有线程加锁成功了,那当前线程最多就等待指定的时间,如果等待了一定的时间之后,还没有获取到锁,那就返回false,表示加锁失败;
只是这里有一个点没搞懂,在同时指定了等待时间和超时时间,那锁的超时时间是等待时间的2倍,并不会使用指定的超时时间
对于redissonLock,在加锁的时候,如果为加锁成功,会订阅指定的channel,然后调用semaphore的tryAcquire方法等待一定的时间,然后再尝试加锁,对于redissonLock没有超时的配置,如果加不到锁,会一直尝试休眠、加锁、休眠