tryLock方法主要可以分为四步:
1、tryAcquire尝试获取锁,如果获取到返回true
2、获取不到锁说明锁被占用了,订阅解锁消息通知
3、收到解锁消息通知,再次尝试获取锁,如果获取不到重复步骤三,直到超过waitTime获取锁失败
4、不论是否获取锁成功,取消解锁消息订阅
通过源码可以看到整个方法内跟获取锁有关的地方只有tryAcquire()这个方法了
public class RedissonLock extends RedissonBaseLock {
// 在waitTime时间范围内尝试获取锁,如果获取到锁,则设置锁过期时间leaseTime
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 = tryAcquire(waitTime, leaseTime, unit, threadId);
// ttl为空说明获取到了锁
if (ttl == null) {
return true;
}
// 判断尝试获取锁是否超过waitTime
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 第二步:订阅解锁消息通知
current = System.currentTimeMillis();
// 订阅锁释放
RFuture subscribeFuture = subscribe(threadId);
// 等待锁释放消息,等待时间超过waitTime,获取锁失败
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
// 如果订阅解锁Future在执行中,等任务执行完后取消订阅锁释放
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
// 取消订阅解锁通知
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
// 判断尝试获取锁是否超过waitTime
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 第三步:自旋尝试获取锁
while (true) {
long currentTime = System.currentTimeMillis();
// 1、尝试获取锁(下文会详细解析此方法)
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// ttl为空说明获取到了锁
if (ttl == null) {
return true;
}
// 判断尝试获取锁是否超过waitTime
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 等待锁释放(信号量控制)
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
// 尝试获取信号量
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
// 判断尝试获取锁是否超过waitTime
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
// 第四步:取消解锁订阅
unsubscribe(subscribeFuture, threadId);
}
}
}
下面我们跟进到这个方法中,看看这里面到底是如何去获取锁的!
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
private RFuture tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture ttlRemainingFuture;
if (leaseTime != -1) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 这里需要注意的是leaseTime==-1,会触发redisson看门狗机制
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// 获取锁成功
if (ttlRemaining == null) {
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
// 锁自动续时(看门狗机制)触发条件leaseTime == -1
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
这里有两个方法值得注意
1、tryLockInnerAsync()这里面是尝试获取分布式锁redis lua脚本,下面会对脚本进行分析
2、scheduleExpirationRenewal()锁自动续时,也就是常说的redisson看门狗机制,后文会对此方法进行详细解析
首先我们看下tryLockInnerAsync()这个方法,可以看到这里面是一个lua脚本,这个就是实现redis分布式锁的核心了,首先解释下脚本变量
EYS[1] = "锁key"
ARGV[1] = "锁过期时间"
ARGV[2] = "当前线程id"
理解了脚本变量意义,我们在看下此方法
RFuture tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
// 如果key一开始就不存在,则直接创建一个key
"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; " +
// 这里是重入锁的实现,同一个线程多次获取锁只需要在value加1即可,value相当于一个加锁计数器
"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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
脚本比较好理解,简单总结一下
1、加锁的key不存在就创建一个redis hash key,field=当前线程id,value=加锁次数
2、有线程持有锁并且未解锁,其他线程是无法获取到锁的
3、加锁成功返回null,加锁失败返回锁过期时间
// 这个是lockInterruptibly和tryLock(time,unit)唯一的区别
// lockInterruptibly,拿不到锁资源,就死等,等到锁资源释放后,被唤醒,或者是被中断唤醒
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 中断唤醒抛异常!
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
// 这个方法可以确认,当前挂起的线程,是被中断唤醒的,还是被正常唤醒的。
// 中断唤醒,返回true,如果是正常唤醒,返回false
return Thread.interrupted();
}