这个是一个Redis框架,提供了各种的分布式服务
使用也很简单
下面来一个小Demo
public class ControllerOne {
private Redisson redisson;
public String doSomething(){
//锁的名字
String lockName = "stock-name";
//使用redisson来进行获取锁,这一步只是获取还没有进行加锁
RLock lock = redisson.getLock(lockName);
try{
String result = "result";
//进行加锁
lock.lock();
//do something
return result;
}finally {
//进行解锁
lock.unlock();
}
}
}
可以看到用起来十分简单,下面就分析一下源码
首先认识一下整体的架构和原理是怎样的
其实整体的架构也就是解决了上一章留下的锁续命问题
认识了原理之后,我们先认识一个概念,LUA脚本
LUA脚本其实是一门脚本语言,Redisson其实就是利用LUA脚本来执行redis命令的,并且Redis在执行LUA脚本时,会当作一个原子性命令去执行
先看第一条代码
这一行代码其实就是实例化想要获取的锁,比如锁的key,因为锁也是一个对象(RLock),在面向对象的语言中必须要先进行实例化才能使用
进入到Redisson里面,它其实就是实例化了一个RedissonLock,并且给了两个参数
具体的构造方法如下
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
//进行实例化父类
super(commandExecutor, name);
//装入命令执行器
this.commandExecutor = commandExecutor;
//设置锁的默认过期时间,默认30S
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
//获取订阅频道
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
从构造方法可以看到,其实所有的配置都在Config类这里
至此,一把锁就被实例化了,产生了一个RedissonLock对象
但RedissonLock是一个具体实现,对外的接口却是RLock,所以先看看RLock的构造
可以看到Rlock多继承了Lock与RLockAsync
再来看看其实现类
可以看到,Redisson支持的锁都实现了RLock,所以都有可重入、异步的功能
我们先看一下它整体的架构
可以看到RedissonLock其实是一个继承的演化过程
对于RedissonLock的构造方法(tryLock的目的其实就是构造一把锁出来)
只有一个构造方法
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
//构造父类
super(commandExecutor, name);
//注入命令执行器
this.commandExecutor = commandExecutor;
//设置锁的过期时间,上面也截图显示这为30S
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
//注入订阅服务(Redis的订阅功能)
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
整体的结构已经看完了,下面就分析一下,lock方法的细节
lock方法其实是重写RLock的
具体调用的lock方法,可以看到这是一个private方法,需要给三个参数
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
//获取当前线程ID
long threadId = Thread.currentThread().getId();
//尝试进行加锁
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
//如果加锁成功,return结束
if (ttl == null) {
return;
}
//加锁失败,为了避免空转,通过订阅频道看能不能获取锁
//所以要先进行订阅频道
//订阅频道
RFuture<RedissonLockEntry> future = subscribe(threadId);
//判断是否优先响应中断
if (interruptibly) {
//优先响应中断并开启开门狗
commandExecutor.syncSubscriptionInterrupted(future);
} else {
//不优先响应中断,并开启看门狗
commandExecutor.syncSubscription(future);
}
//开始进行自旋加锁
try {
//死循环
while (true) {
//再次进行加锁
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
//加锁成功,break跳出循环
if (ttl == null) {
break;
}
//加锁失败,并且返回的锁的过期时间>=0
if (ttl >= 0) {
try {
//锁拥有过期时间,使用AQS来避免空转和判断线程是否被中断了
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
//假设线程处于中断状态,且优先处理中断
if (interruptibly) {
//抛出异常
throw e;
}
//假设线程处于中断状态,但并不优先处理中断
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
//ttl < 0代表锁已经消失
//只需要判断是否优先处理中断
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
//最终拿到锁就退出订阅的频道
//进行退订操作
unsubscribe(future, threadId);
}
}
从这源码上,可以看到大概的逻辑如下
通过上面的代码可以知道,tryAcquire是去获取锁的,下面就看看这个tryAcquire做了什么
先去执行了tryAcquireAsync方法,然后再执行get方法
源码如下
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
//定义一个RFuture,其实RFuture可以理解成是客户端,里面可以保存Redis执行命令的结果
RFuture<Long> ttlRemainingFuture;
//如果锁的释放时间为-1,代表使用默认值
if (leaseTime != -1) {
//执行自定义过期时间去获取锁方法
//将返回的结果存入RFuture中
//给到的参数有等待时间、释放时间、时间单位,线程ID和Reids的EVAL命令
//eval命令就是用来执行lua脚本的
//这里要注意,Redisson将所有Redis的命令存进了RedisCommands里面
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//如果为-1,代表使用默认的过期时间
//使用默认的加锁方法
//同理将结果存入RFuture中
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
//处理一下获取的结果
//lambda表达式,参数为一个BiConsumer接口,消费者接口(没有返回值,有参数,用来对参数进行处理的)
//并且为BiConsumer,可以提供两个参数进行消费处理(这里给了一个)
//onComplete方法就是开启了watch dog
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
//这下面就是接口的accept方法逻辑
//假如e不为空,直接返回
//代表获取失败,并且是出现了异常!!!
//因为e是一个异常
if (e != null) {
return;
}
// lock acquired
//假如没有异常且ttlRemainning为null,代表获取锁成功了!!!
if (ttlRemaining == null) {
//判断过期时间是否为默认的,即判断是否为默认配置
if (leaseTime != -1) {
//不是默认配置的话,将过期时间转换为毫秒
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
//如果是默认配置的,开启定时任务重置过期时间
scheduleExpirationRenewal(threadId);
}
}
//假如ttlRemainning不为null,代表获取锁失败
});
//做完处理后,返回整个获取的结果
return ttlRemainingFuture;
}
从这步获取锁的代码中可以看到以下点
这个方法是用来尝试获取锁的,也就是这里执行底层的LUA脚本
终于快看到底层了,我都快裂开了。。。。
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
//执行LUA脚本去获取锁
//传入这里的参数为
//1. waitTime:等待时间,-1代表不会进行等待
//2. leaseTime:释放锁的时间,即锁的过期时间
//3. unit:时间单位
//4. threadId:线程ID
//5. command:传进来的Redis命令,这里传进来的是EVAL,执行LUA脚本的命令
return evalWriteAsync(getRawName(), 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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
可以看到这个方法,调用了evalWriteAsync方法,不过从这个方法里面,我们可以提出到重要信息
脚本的内容如下,也就是获取锁执行的命令
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]);
下面来这段脚本到底在干什么的,不过首先我们要认识存储锁的底层是怎样的
存储锁的底层其实是一个hash结构,这个结构可以理解成锁,,比如仓库的所有(其key值其实就是执行tryLock方法给的lockName),而里面的键值对代表是哪个线程拥有了这把锁,存取的是线程用锁清空,键值对的key对应的就是上面所说的getKeyName方法返回的值(组成为clusterID+:+线程ID),而value是与可重入锁相关的,这个线程可以对这把锁进行重入加锁
这个方法是专门用来执行lua脚本的
protected <T> RFuture<T> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {
//创建CommandBatchService,用来批量去执行命令的
CommandBatchService executorService = createCommandBatchService();
//通过CommandBatchServic去异步执行脚本
//并且将客户端存储的结果封装在RFuture中
RFuture<T> result = executorService.evalWriteAsync(key, codec, evalCommandType, script, keys, params);
//ComandBatchService的创建
//假如commandExecutor可以强转成CommandBatchService
//那么上面创建的CommandBatchService就是由commandExecutor强转成的
if (commandExecutor instanceof CommandBatchService) {
return result;
}
//
RPromise<T> r = new RedissonPromise<>();
RFuture<BatchResult<?>> future = executorService.executeAsync();
future.onComplete((res, ex) -> {
if (ex != null) {
r.tryFailure(ex);
return;
}
//
r.trySuccess(result.getNow());
});
return r;
}
从这里可以看到,执行LUA脚本的是由CommandBatchService来负责的,并且一般来说,这个CommandBatchService是由注入的CommandExecutor强转而来,也就是本质是由CommandExecutor来执行的!!!
把加锁的具体细节都看完了,现在也可以知道为什么前面用long变量接收加锁的结果了,前面产生的一些疑问也随之解开了
回到tryAcquireAsync方法中,接下来就是执行ttlRemainingFuture.onComplete方法了(ttlRemainingFuture是封装加锁的结果的)
一直到onComplete方法底层
这个方法就是开启watchDog的
源码如下
protected void scheduleExpirationRenewal(long threadId) {
//ExpirationEntry是用来封装被监视的线程的!!!
ExpirationEntry entry = new ExpirationEntry();
//EXPIRATION_RENEWAL_MAP容器是用来存储被监视的锁的
//通过putIfAbsent方法判断容器中是否已经有该锁了
//getEntryName返回的是entryName,组成为custerId:lockName(tryLock给的锁的名字)
//从这里也知道了RedissonBaseLock的entryName是用来映射监视锁容器的对应value的
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
//如果容器中已经存在该锁了
if (oldEntry != null) {
//往旧锁中添加线程ID
oldEntry.addThreadId(threadId);
} else {
//往新锁中添加线程ID
entry.addThreadId(threadId);
try {
//开启线程去进行锁续命
renewExpiration();
} finally {
//最后判断线程是否被中断
if (Thread.currentThread().isInterrupted()) {
//如果被中断,取消锁续命
cancelExpirationRenewal(threadId);
}
}
}
}
总结一下
看一下ExpirationEntry的底层线程容器
该方法就是开启定时任务,也就是WatchDog去进行锁续命的
private void renewExpiration() {
//从容器中去获取要被续命的锁
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
//容器中没有要续命的锁,直接返回null
if (ee == null) {
return;
}
//创建定时任务
//并且执行的时间为30000/3毫秒,也就是10秒后
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;
}
//Redis进行锁续命
//这个方法的作用其实底层也是去执行LUA脚本
RFuture<Boolean> future = renewExpirationAsync(threadId);
//同理去处理Redis续命结果
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
//如果成功续命,递归继续创建下一个10S后的任务
if (res) {
// reschedule itself
//递归继续创建下一个10S后的任务
renewExpiration();
}
//假如有一次失败,那就会取消续命
else {
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
这个方法就是真实进行锁续命的,而且从方法名上看,还是一个异步的动作
源码如下
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), 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(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
脚本的内容如下
//判断锁是否被人占用,即hash里面的键值对要等于1
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
//对锁进行锁续命,这里是对hash对象进行续命
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end;
return 0;
续命的过程大概如下
续命的时间是传的参数internalLockLeaseTime(已经看到为30S),也就是默认的锁释放时间,拓展一下,pexpire和expire的作用是一样的,就是重新对key设置过期时间,但pexpire是毫秒为单位,expire是秒为单位
至此整个tryAcquireAsync方法已经看完了,返回到tryAcquire方法,接下来就是执行get方法了
从源码上可以看到,这个方法是来自RedissonObject的,可以用来取出封装在RFuture的结果
这里的结果就是加锁的结果,加锁成功就为nill,加锁失败就为锁的过期时间
现在回到Lock方法里面。现在我们知道了,tryAcquire方法就是尝试进行加锁并且加锁成功返回nil,加锁失败返回锁的过期时间的
在前面的lock方法中,对于自旋获取锁失败后,会调用该方法,下面就看看这个方法是做什么的
首先这里要注意,future此时是进行了订阅命令的,保存着订阅监听的结果
这里使用了future,下面我们就对RFuture这个接口分析一下,这个接口是用来表示异步计算的结果,并且其继承了Concurrent的Future接口(也是用来表示多线程异步计算结果的)
所以,这里可以理解为RFuture存放了Redis计算的结果
源码如下
从源码中可以看到,其使用了Concurrent的Semaphore对象,这个对象是一个线程同步的辅助类,可以维护当前访问自身的线程个数,而且可以其调用的tryAcquireSharedNanos方法是AQS实现的!!!
也就是说,这里空转的实现本质竟然是AQS,AQS是一个多线程访问的资源的共享框架,而执行方法的作用是共享式地去获取资源,并且优先响应标志位中断
前面已经认识了lock方法里面的参数interruptiably是让给线程加锁的过程中要优先响应中断,从这里就可以看到,优先响应中断的本质是通过AQS来实现的
而且AQS还有一个作用就是,它底层是实现就是自旋+CAS,并且有对避免空转的支持,所以RedissonLock的lock方法优先响应标志位中断和避免空转是通过AQS去实现的,并且这里空转的时间由ttl决定,并且ttl的含义是,如果加锁成功就为Null,如果加锁失败就为锁(hash对象)的过期时间,所以可以认为,这里空转的时间为锁的过期时间
并且这里甚至做了个比较特别的处理,因为一般ttl如果加锁失败,都会返回正数的过期时间,不过从源码上可以看到,是有对ttl为负数的处理的
而这里对ttl小于0的处理也很简单,因为ttl小于0代表锁并不存在了,那么只需要判断当前线程需不需要优先响应中断即可(也是由AQS去支持实现)
终于走到lock方法最后的unsubscribe了
从前面可以知道,避免空转的实现是AQS,那么也可以猜测到subscribe和unsubscribe方法大概也是关于AQS的