Redisson有一个功能是实现分布式锁,本文来讨论一下这个功能的特点以及源码分析。
1.互斥
2.高可用
3.防死锁(有超时控制机制或者撤销功能)
4.阻塞、非阻塞
5.可重入
等等。可见实现一个分布式锁要考虑很多,接下来我们通过简单的业务代码思考一下,看看redisson如何实现。建议看本博客之前打开idea,一边看博客,一边看源码好一点。
下面所展示的是比较简单的业务代码,以此来标志redisson源码的入口:
public static void main(String[] args)
{
RLock redissonLock = redisson.getLock("redisson-lock");
redissonLock.lock();
try
{
log.info("开始执行业务");
//模拟业务代码执行
TimeUnit.SECONDS.sleep(25);
log.info("业务执行结束");
}catch (Exception e){
e.printStackTrace();
}finally {
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
{
redissonLock.unlock();
}
}
System.out.println(Thread.currentThread().getName() + "main线程结束");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
redisson.shutdown();
}
这段代码的执行结果不言而喻。那么问题就来了,首先我并没有给定锁的过期时间,那么redisson是如何知道我需要多久的过期时间呢?其次,可以看到执行完lock方法之后并没有进行停留,直接进行到了业务方法。那么对于第一个问题,由于lock方法是有重载方法的,可以设置时间,那么可以确定,在不指明时间的前提下,lock方法是会给锁一个默认的过期时间。它又如何确定我业务执行时间以保证在我业务执行期间内别的线程不会进行争抢呢?这就是看门狗机制解决的问题。而且肯定是另起线程进行异步监听的。至于监听的是什么,怎么监听?请带着问题进行源码分析。
进入lock方法底层后我们可以看到:
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
//当前线程id
long threadId = Thread.currentThread().getId();
//AQS经典方法 tryAcquire ttl:锁剩余时间 如果ttl为null表示分布式锁获取成功,否则获取失败
//后续详解tryAcquire方法
Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
if (ttl != null) {
//订阅分布式锁释放事件,通过Semaphore控制并发访问
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
//根据线程是否可被中断,用不同的方式阻塞线程,等待分布式锁释放事件
if (interruptibly) {
this.commandExecutor.syncSubscriptionInterrupted(future);
} else {
this.commandExecutor.syncSubscription(future);
}
//----分布式锁释放----
try {
while(true) {
//再次尝试获取锁资源
ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
//获取成功,退出循环
if (ttl == null) {
return;
}
//获取失败,抢锁的逻辑实现,通过semaphore实现,只会获取许可证,并不会真正抢到锁
if (ttl >= 0L) {
try {
/*
这里线程的阻塞时间刚好是锁的过期时间,要么在阻塞时间内被唤醒,要么超时返回false,继续循环抢锁
*/
((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);
}
}
}
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
//leaseTime = -1;
if (leaseTime != -1L) {
return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//返回异步任务,该方法详解见下图
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(
waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS,
threadId,
RedisCommands.EVAL_LONG);
//---------------------------------------
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
//判断异步任务是否执行成功
if (e == null) {
//判断是否加锁成功,等于null加锁成功
if (ttlRemaining == null) {
//给锁设置过期时间并进行续期
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
//将锁的过期时间转为ms
this.internalLockLeaseTime = unit.toMillis(leaseTime);
/*
执行lua脚本的方法,返回值的T由RedisStrictCommand command 决定,由于之前传入的是 RedisCommands.EVAL_LONG,所以T为long类型
*/
return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,
// lua脚本
"
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,
//在redis中,当前线程的唯一标识
this.getLockName(threadId));
}
这段Lua脚本是Redisson分布式锁的实现,我们先说一下这个lua脚本中参数的含义:
至于前面的hexists,pexpire,hincrby这些都是redis中的原生命令,因此这个lua脚本就不难理解了,想要执行这些原生命令,后面总得有参数吧。使用lua脚本也是为了保证其原子性。
具体来说,这段Lua脚本包含以下几个步骤:
由此可见,redisson分布式锁也是一把可重入锁。
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
/*
this.getEntryName():返回当前对象的entryname,默认为uuid + 锁名,在MasterSlaveConnectionManager这个类的构造器中可以了解到id
如果返回的oldentry不为空,证明之前当前线程已经加过锁了,因为当前类并未使用单例模式,每个线程进来都会产生一个uuid,如果之前这个map中已经存在,则证明已经加锁。
*/
ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
//表示当前线程是重入加锁
oldEntry.addThreadId(threadId);
} else {
//表示当前线程是第一次加锁
entry.addThreadId(threadId);
this.renewExpiration();
}
}
private void renewExpiration() {
ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
//timeout代表了一个延迟任务,在指定时间后执行 **一次** 某个任务
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = (ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
//使用lua脚本进行续期
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) {
//timeout的run方法只会被执行一次,所以进行递归
RedissonLock.this.renewExpiration();
}
}
});
}
}
}//间隔时间 10s执行一次
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
1.redisson如何监控锁是否过期?或者redisson如何监控业务线程是否执行完业务?
redisson并没有监控业务线程是否执行完任务,当你调用unlock方法或者shutdown redisson后会停止刷新锁的过期时间,当业务中并没有调用unlock方法或者redisson的shutdown方法并且在不出异常的情况下,那么该锁在redis中就一直存活。
以上都是本人的拙见,有大佬觉得有错误的地方请在评论区指正,谢谢!