一、Redisson的使用方式
Redisson是分布式架构中常见的分布式锁的一种实现形式,在使用的过程中,一般有两种方式,一中是带过期时间的,另一种是不带过去时间的,以及对应的源码的处理上到底有什么差异呢?我们一起探究一下。
1.1 使用方式一,带过期时间
以下是带过期时间的常见实用方式,虽然源码分析中是在使用方式一的流程中讲到了看门狗机制,但是过期时间填了具体的值,而不是填 -1,例如30秒,这种情况下是不会触发看门狗机制的,参考2.3处的源码
RLock lock = redissonClient.getLock(lockKey);
boolean tryLock = false;
try {
tryLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (tryLock) {
//TODO 逻辑处理
}
} catch() {
//异常处理
} finally {
if (tryLock) {
lock.unlock();
}
}
1.2 使用方式二,不带过期时间
以下是不带过期时间的使用方式
RLock lock = redissonClient.getLock(lockKey);
boolean tryLock = false;
try {
tryLock = lock.tryLock(10, TimeUnit.SECONDS);
if (tryLock) {
//TODO 逻辑处理
}
} catch() {
//异常处理
} finally {
if (tryLock) {
lock.unlock();
}
}
注意:以上是两种使用方式,区别在于方式1加了过期时间,方式2没加过期时间,然而内部的处理逻辑是不一样的。Redisson作为一个分布式锁,其加锁/解锁的逻辑才是我们关注的核心,还有一种使用方式是调用lock()方法,但是这种方式使用相对少,在分析完tryLock的源码后进行分析,
tryLock旨在了解其加锁过程,个人认为并不需要每行代码都弄明白啥意思。掌握了其加锁的核心思想即可。
二、源码解析
2.1 tryLock方法源码
tryLock()源码如下
/**
* 加锁方法源码
* @param waitTime 加锁等待时间
* @param leaseTime 锁的有效时间
* @param unit 时间单位
* @return true:加锁成功 false:加锁失败
* @throws InterruptedException 异常
*/
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
//将等待时间转成毫秒数
long time = unit.toMillis(waitTime);
//系统时间毫秒数
long current = System.currentTimeMillis();
//当前线程id
long threadId = Thread.currentThread().getId();
//加锁核心逻辑
Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
//ttl为空,表示加锁成功,返回true
if (ttl == null) {
return true;
} else {
time -= System.currentTimeMillis() - current;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
return false;
} else {
current = System.currentTimeMillis();
RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
this.unsubscribe(subscribeFuture, threadId);
}
});
}
this.acquireFailed(waitTime, unit, threadId);
return false;
} else {
boolean var14;
try {
time -= System.currentTimeMillis() - current;
if (time > 0L) {
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)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
} while(time > 0L);
this.acquireFailed(waitTime, unit, threadId);
var16 = false;
return var16;
}
this.acquireFailed(waitTime, unit, threadId);
var14 = false;
} finally {
this.unsubscribe(subscribeFuture, threadId);
}
return var14;
}
}
}
}
2.2、tryAcquire方法
该方法之后的逻辑是核心方法,可以细读源码逻辑。该方法调用tryAcquireAsync方法,加锁核心逻辑在该方法中
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
2.3、tryAcquireAsync方法
方法源码如下,从源码中可以看出一个非常重要的点,如果你上锁时间填了具体的时间,例如过期时间填了30秒这样,是不会触发看门狗机制的,这点需要特别注意,要出发看门狗机制,又想填过期时间,可以填 -1。
/**
* 尝试加锁
*
* @param waitTime 等待时间
* @param leaseTime 过期时间
* @param unit 时间单位
* @param threadId 线程id
* @param <T>
* @return 泛型类
*/
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
//判断过期时间是否不等于 -1,如果不指定leaseTime就是默认-1
if (leaseTime != -1L) {
//加锁,这里加锁结果是直接返回,并不存在看门狗机制
return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//如果未指定过期时间或者过期时间指定为-1,进入改处理逻辑
//this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout():设置默认过期时间为30秒(lockWatchdogTimeout的值)
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
//这里就是看门狗机制源码,监听线程id
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
//进行过期时间延长处理
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
2.4、tryLockInnerAsync方法
源码如下
/**
* 加锁底层原理-lua脚本
*
*
*/
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
//加锁的核心是通过lua脚本。lua脚本的解释可以自行百度一下
return this.evalWriteAsync(this.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(this.getName()),
this.internalLockLeaseTime,
this.getLockName(threadId));
}
2.5、scheduleExpirationRenewal方法
该方法可以看到Redisson的看门狗机制,也是比较重要
/**
*看门狗机制
*/
private void scheduleExpirationRenewal(long threadId) {
RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
//看门狗机制续时间逻辑
this.renewExpiration();
}
}
2.6、renewExpiration方法
该方法实现了看门狗机制续约时间源码
/**
* 看门狗机制续时间的源码
*/
private void renewExpiration() {
RedissonLock.ExpirationEntry ee = (RedissonLock.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 {
RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
} else {
if (res) {
RedissonLock.this.renewExpiration();
}
}
});
}
}
}
},
//每次续的时间数是默认过期时间 / 3,就是每次续10000毫秒,10秒钟
this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
三、troLock的方式2的源码
这是不带过期时间的使用方式,推荐使用这种,这种方式是可以触发Redsson看门狗机制的,可以不去过多的考虑时间到了,但是业务还没有执行完的情况
源码如下
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
//实际上也是调用方式1的tryLock方法,区别在于leaseTime传递的是-1,这种加锁方式可以触发看门狗机制,不用担心业务还在执行,却释放了锁的情况
return this.tryLock(waitTime, -1L, unit);
}
//lock方法源码
public void lock() {
try {
//调用lock()方法,等待时间传的是-1
this.lock(-1L, (TimeUnit)null, false);
} catch (InterruptedException var2) {
throw new IllegalStateException();
}
}
//lock的加锁源码,核心在于有注释的几行代码,与tryLock的区别在于没有等待时间的概念,
//可以触发看门狗机制,默认过期时间和时间续费逻辑一样
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
//当前线程id
long threadId = Thread.currentThread().getId();
//先尝试加锁
Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
//表示加锁失败
if (ttl != null) {
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
if (interruptibly) {
this.commandExecutor.syncSubscriptionInterrupted(future);
} else {
this.commandExecutor.syncSubscription(future);
}
try {
//while(true)处理,也可以成为自旋锁
while(true) {
//加锁逻辑
ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
表示加锁成功
if (ttl == null) {
//加锁成功,结束方法
return;
}
if (ttl >= 0L) {
try {
((RedissonLockEntry)future.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException var13) {
if (interruptibly) {
throw var13;
}
((RedissonLockEntry)future.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else if (interruptibly) {
((RedissonLockEntry)future.getNow()).getLatch().acquire();
} else {
((RedissonLockEntry)future.getNow()).getLatch().acquireUninterruptibly();
}
}
} finally {
this.unsubscribe(future, threadId);
}
}
}