某些方法执行非常耗时,如果超过了指定时间,希望Redisson能自动释放这把锁,方便别的服务或者线程重新获取锁资源,可以使用leasetime这个参数。
lock.lock(10, TimeUnit.SECONDS);
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
RLock lock = redisson.getLock("anyLock");
底层对应代码:
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.id = commandExecutor.getConnectionManager().getId();
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = id + ":" + name;
}
创建了一个RedissonLock,包含了: commandExecutor、id、internalLockLeaseTime以及entryName四个参数。
① commandExecutor看起来像是用来与Redis沟通的命令执行器。
② id就是一串UUID,由UUID.randomUUID()获得。用于唯一标识当前的这个客户端。
③ internalLockLeaseTime 看门狗每次为锁重新设定有效期的时长,默认是30秒。
④ entryName: id和name拼起来的字符串。这玩意有什么用呢?
Tips1: id的作用,既然是分布式系统,肯定会有多个客户端希望对这个KEY加锁吧,Redis如果能知道这个KEY是由哪个客户端加的锁,不仅能在锁被释放之前,拒绝其它客户端对这个KEY的加锁操作,而且如果之前加锁的客户端再次加锁,那么Redis还能轻松的识别出这个客户端,并对本次加锁操作返回true(当然了,这里只是可重入锁校验的一个部分,还需要校验是不是同一个线程加的锁)。
疑问: 感觉不太严谨,为何会redisson会选择使用version4的UUID创建方式呢?毕竟这可是会有重复风险的。
lock.lock();
进入lock()方法
@Override
public void lock() {
try {
lockInterruptibly();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
进入lockInterruptibly()方法
lockInterruptibly(-1, null);
第一个参数:leaseTime 默认值为-1,意思是说,当前获得的这把锁,永不过期。
第二个参数:unit 单位呗。
继续向下进入源码,tryAcquire()方法就是在尝试获取锁
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
return;
}
KEYS: 数组,存放了待加锁的锁名称。
ARGV:数组,存放了看门狗每次为锁续约的时长、[随机数:线程数] 一个客户端上的一个线程,对这个KEY加锁的唯一标识。
底层就是搞了一段lua脚本,实现了加锁的逻辑,翻译成中文就是:
if (待加锁的KEY在Redis集群内不存在) {
在名为"anyLock"的Map中,添加<当前客户端的唯一标识+":"+线程id , 1>的键值对;
为名为"anyLock"的Map设置有效期,时长是30秒;
return null;
}
if (锁在Redis集群内存在,并且锁对应Map的KEY = 当前客户端的唯一标识 + ":" + 线程id) {
把名为"anyLock"的Map中,KEY=[uuid+":"+线程id]的值加1。
为名为"anyLock"的Map设置有效期,时长是30秒;
return null;
}
return "anyLock"这把锁的剩余过期时间;
上面lua脚本中的1,就是KEY加锁的次数。
看看commandExecutor.evalWriteAsync()
@Override
public <T, R> RFuture<R> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType,
String script, List<Object> keys, Object... params) {
NodeSource source = getNodeSource(key);
return evalAsync(source, false, codec, evalCommandType, script, keys, params);
}
首先,使用CRC-16算法计算待加锁的KEY,接着,对16384进行取模,计算出KEY存放的slot。
int result = CRC16.crc16(key.getBytes()) % MAX_SLOT;
找到这个slot存放在Redis集群内的哪个master节点上。entry包含了redis master节点的信息,比如部署的IP地址,端口号。
MasterSlaveEntry entry = connectionManager.getEntry(slot);
所以,NodeSource就是一个Redis Master节点。
现在已经知道了,需要把用于加锁的lua脚本,传递到哪一台Redis Master上执行,完成整个加锁的操作。
使用netty,与目标Redis master进行通信
值得一提的是,在加锁的过程中,会返回一个ttl,代表当前锁的剩余有效时长,成功获得锁的客户端会发现,这个ttl返回的是null,其它客户端会发现,这个ttl返回的是一个大于0的数字。
Watch Dog的触发方式 RedissonLock #tryAcquire()
前面说了,尝试获取锁的代码是RedissonLock #tryAcquire(),返回的结果是锁的剩余有效期。考虑到这段代码是异步执行的,Redisson在Future上加了一个监听器,一旦异步操作执行完毕,就会回调这个监听器的operationComplete()方法。
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
##### 如果查看锁有效期失败,那就没必要继续往下执行了 #####
return;
}
Long ttlRemaining = future.getNow();
##### 如果锁的有效期等于null,那就说明当前线程已经拿到了锁,此时就会走看门狗的那套逻辑了 #####
if (ttlRemaining == null) {
##### 看门狗业务逻辑,执行的入口 #####
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
Watch Dog的工作原理 scheduleExpirationRenewal(threadId);
private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}
if (future.getNow()) {
// reschedule itself
scheduleExpirationRenewal(threadId);
}
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
task.cancel();
}
}
每次创建出task后,需要延迟10秒才会执行续约,如果续约操作执行失败,则把当前的锁扔到"已经过期不需要续约"的集合中。如果续约操作成功,则递归调用scheduleExpirationRenewal()方法,再次等待10秒,继续尝试续约。
PS: 这个定时任务挺搞笑的,搞了一个任务,每隔10秒跑一次,每次执行完业务逻辑后,再做一次递归调用。
看看renewExpirationAsync()方法,它会检查当前线程持有的锁对应的KEY在Redis中是否仍然存在,若存在,则重新设置KEY的有效时长为30秒。想想看,KEY由服务实例的唯一标识和线程id组合而成,每次为锁重新设定的有效时长都是30秒,我们现在每隔10秒去检查一次,如果说线程没有主动的释放锁,那么这个KEY一定是存在的,反之,KEY如果不存在,则锁一定是被原本持有锁的服务实例的线程主动给释放了。
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return commandExecutor.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.<Object>singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
既然是释放锁,那就调用RLock的unlock()方法吧。
lock.unlock();
进入unlock(),与lock()的风格完全一样,有一种对称的美~
@Override
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException)e.getCause();
} else {
throw e;
}
}
}
get()方法只不是是把内部对netty的调用,异步转同步而已,所以真正的业务逻辑还是得看unlockAsync()。
@Override
public RFuture<Void> unlockAsync(final long threadId) {
final RPromise<Void> result = new RedissonPromise<Void>();
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
if (!future.isSuccess()) {
cancelExpirationRenewal(threadId);
result.tryFailure(future.cause());
return;
}
Boolean opStatus = future.getNow();
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
if (opStatus) {
cancelExpirationRenewal(null);
}
result.trySuccess(null);
}
});
return result;
}
一看就明白了,释放锁的请求肯定是由unlockInnerAsync()完成的,返回的是一个RFuture,这东西肯定是异步的,所以也是做了一个监听器,一旦异步操作执行完毕,就会执行operationComplete()。
所以还是看看unlockInnerAsync()吧。
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(
getName(),
LongCodec.INSTANCE,
RedisCommands.EVAL_BOOLEAN,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
"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.<Object>asList(getName(), getChannelName()),
LockPubSub.unlockMessage,
internalLockLeaseTime,
getLockName(threadId));
}
底层就是搞了一段lua脚本,实现了释放锁的逻辑,翻译成中文就是:
if (锁是否存在? 不存在则进入分支) {
向名为"redisson_lock__channel:{anyLock}"的Channel中发布一个解锁的消息;
return 1;
}
if (当前Redis中,是否存在这把锁,并且是不是当前发起请求的这台服务实例上的这个线程所持有的锁? 不是则进入分支) {
return nil;
}
// 代码能运行到此处,说明这把锁一定是当前这台服务实例上的这个发起请求的线程持有的锁。
// 获取当前这把锁的被重复加锁的次数。这里首先减去1,然后再返回次数。
if (重入锁的次数是否大于0? 大于0则进入分支) {
// 说明这不是这把锁第一次被加锁了,这次只是释放了一次锁,还得等以后继续释放锁
重新设置锁的存活时间
return 0;
}
else {
删除锁
向名为"redisson_lock__channel:{anyLock}"的Channel中发布一个解锁的消息;
return 1;
}
end;
// 保险起见,其它情况下,直接返回nil,但是我想也不会有其他可能吧... 毕竟刚刚if/else,总会走一个分支的。
return nil;
首先需要说明,如果我们使用的是lock.lock(); 那么就算此时已经有其它人持有锁,我们也不会超时,因为首次获取锁失败后,会进入while(true)无限循环,不断的尝试释放锁、等待获取锁。
因此,只有当我们使用指定锁超时时间的方式获取锁时,才会产生超时的问题。
boolean acquireTime = lock.tryLock(100, 10, TimeUnit.SECONDS);
上述语句有两层含义:
通过tryLock的有参方法获取锁时,会为本次获取锁设置一个总的时长,并尝试获取锁,如果获取失败,则进入无限循环,
尝试获取锁->计算获取锁的剩余时间->发现锁的剩余时间大于0->等待一段时间->再次尝试获取锁。
若在指定时间范围内没有成功获取锁,由于锁的剩余时间小于等于0,则tryLock()方法将返回false。
ps: 每次获取锁之前的等待时长与锁的剩余生命周期,以及获取锁的剩余时间有关,如果锁的剩余时间富裕,则等待锁的剩余生命周期这么长时间,再去获取锁时成功的概率会更大(对Redis的请求压力也更小),但是如果获取锁的剩余时间寥寥无几了,那就得抓紧时间,赶在获取锁的剩余时间为0之前,再次尝试获取一次锁。
一旦使用了有参的tryLock()获取锁,那么在成功获取锁后,不会创建看门狗,也就不会有每隔一段时间重置锁生命周期的操作了。所以,锁的生命周期在创建的时候已经被定义好,比如10秒,那么过了10秒后,
答: 每隔10秒检查一次,若锁仍然被持有,则重新设定锁的有效期为30秒。
答: 起初我非常疑惑,在获取锁,也就是向Redis插入Map时,明明设置了30秒的有效时长,并且我在redis-cli中,通过pttl命令也能看到这个KEY对应的有效时长,那么为什么Redisson中返回的结果是null呢?当看完了加锁的lua脚本和RedissonLock #lockInterruptibly()后,一切就变得非常简单。如果lua脚本执行成功,就会返回null,RedissonLock的加锁逻辑就直接返回了,因为这意味着加锁已经成功。
答: 对于这个问题,我们分情况讨论。
① 线程获得锁时设置了释放锁的时间,比如5秒,假设在获得锁3秒后线程挂掉,则再过2秒后,锁将自动被释放(KEY自动被Redis回收)。
② 线程获得锁时,没有设置锁的释放时间,假设在获得锁3秒后线程挂掉,则默认再过27秒,锁将被自动释放(KEY自动被Redis回收)。为什么是27秒,因为Redission创建锁时,默认为锁赋予的过期时间就是30秒,30-3=27。(对应lockWatchdogTimeout参数)
答: 针对于同一条线程多次加锁时,"anyLock"锁对应的Map内,KEY对应的值,也就是加锁的次数会累加1。此外,锁的过期时间被重置为30秒。
答: 既然是加锁,那么一定会执行commandExecutor.evalWriteAsync()那段lua脚本。我们来回顾一下。
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', 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]);"
这段lua脚本由两个if/else组成,
首先看看第一个if/else,由于锁未被释放,所以锁肯定存在,分支逻辑不会执行。
接着再来看看第二个if/else,由于锁未被释放,所以肯定存在,但是这个逻辑表达式还需要判断锁对应的Map内的KEY与当前传入的ARGV[2]是否相同,只有相同,才能执行分支逻辑。想想看,ARGV[2]不就是"分布式锁的随机数:线程id"么,对于同一个客户端而言,这把分布式锁的随机数的确是相同的,但是线程id不同啊,所以这个逻辑表达式不可能成立。对于另一个客户端而言呢,由于连分布式锁的随机数的都不一样,所以逻辑表达式更不可能成立。
于是,lua脚本返回的是这把锁的存活时间。然后,再回顾一下执行完lua脚本后的肯定会运行的监听器。
ttlRemainingFuture.addListener(new FutureListener<Long>() {
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
}
});,
显而易见的,ttlRemaining返回的肯定就不是null了,所以创建定时任务的操作(对应着scheduleExpirationRenewal()方法)就不会执行了,也就不会定时刷新锁的生存时间了。
既然获取锁失败,那么当前线程是不是会放弃获取锁了呢?当然不是。
回到RedissonLock的lockInterruptibly()。
说简单点,就是进入一个死循环,无限的重复两个操作: ①再次获取锁 ②等待一段时间(尝试获取锁时返回的剩余时间)。
直到获取到锁。
答: 这就是利用Redis的hash结构,key是锁的名称,field是锁的标识(或者说是归属),value是被重复加锁的次数。
公平与否体现在申请锁的顺序和最终获得锁的顺序是否一致。
对于非公平锁而言,当锁被某个客户端持有时,其它客户端会不断地争抢这把锁,没有所谓的先来后到的机制,素质极差。
对于公平锁而言,当锁被某个客户端持有时,其它客户端对这把锁的获取请求会被排序,在短时间内,不会因为某个客户端请求次数多,就让它插队,而是严格的按照最初申请锁的顺序,在锁被释放后,分配锁的归属。比如A持有了锁,B和C都来争抢这把锁,假设C先发起了请求,B后发起了请求,那么加锁的顺序就是先C后B,就算B随后在一定的时间范围内(比如5秒内)发起了10000条获取锁的请求也无济于事,当A释放锁后,C先跑过来请求获取锁,此时会发现自己没有排在第一个位置,所以无法获取到锁,接着B过来获取锁,由于B排在第一个,所以就能成功获得锁。
以下是使用公平锁时的代码
RedissonClient redisson = ...
RLock failLock = redisson.getFailLock("anyLock");
failLock.lock();
...
failLock.unlock();
属于非公平锁的代码RedissonFailLock继承了RedissonLock,仅从类的继承关系上就能体现出,公平锁其实就是非公平锁的高级应用。因此,在获取锁时,公平锁也有着设定超时时间和不设定时间两种做法,区别就是后者获取锁时,会多加一个监听器,当成功获得锁后,会创建一个watch dog,用于锁的续约。
那么公平锁与非公平锁的区别在哪里呢?其实就在获取锁的方式上,让我们看到获取锁的代码
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
代码非常熟悉,重点看tryLockInnerAsync( ),默认情况下,RedissonLock提供了这个方法的实现(它的实现逻辑就是非公平锁),但是RedissonFailLock重写了这个方法。
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
long currentTime = System.currentTimeMillis();
if (command == RedisCommands.EVAL_NULL_BOOLEAN) {
return 一大坨代码...
}
if (command == RedisCommands.EVAL_LONG) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"while true do "
+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
+ "if firstThreadId2 == false then "
+ "break;"
+ "end; "
+ "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
+ "if timeout <= tonumber(ARGV[4]) then "
+ "redis.call('zrem', KEYS[3], firstThreadId2); "
+ "redis.call('lpop', KEYS[2]); "
+ "else "
+ "break;"
+ "end; "
+ "end;"
+ "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "
+ "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
"redis.call('lpop', KEYS[2]); " +
"redis.call('zrem', KEYS[3], ARGV[2]); " +
"redis.call('hset', 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; " +
"local firstThreadId = redis.call('lindex', KEYS[2], 0); " +
"local ttl; " +
"if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " +
"ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" +
"else "
+ "ttl = redis.call('pttl', KEYS[1]);" +
"end; " +
"local timeout = ttl + tonumber(ARGV[3]);" +
"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
"redis.call('rpush', KEYS[2], ARGV[2]);" +
"end; " +
"return ttl;",
Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName),
internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime);
}
throw new IllegalArgumentException();
}
command == RedisCommands.EVAL_NULL_BOOLEAN的这段if逻辑没必要看,因为主流程获取锁时,传递的都是RedisCommands.EVAL_LONG。
所以仔细分析第二个if逻辑吧。
KEYS数组:
【1】锁的名称:anyLock
【2】为这把锁创建的队列的名称:redisson_lock_queue:{anyLock}
【3】为这把锁创建的Set集合的名称:redisson_lock_timeout:{anyLock}
ARGV数组:
【1】默认每次续约锁的存活时间:30 * 1000毫秒
【2】当前服务实例、当前线程获取到锁后的唯一标识:UUID:threadId
【3】当前时间 + 线程等待时间(5000毫秒)
【4】当前时间
至少要积累以下Redis命令相关知识,不然后面的lua脚本很难看懂。
假设名称为anyLock的锁从未被持有,现在是第一次加锁。
进入while循环,由于队列是空的,所以lindex获取到的firstThreadId2为空,因此break迅速离开while循环。
接着,开始判断锁是否存在,由于是第一次加锁,显然锁不存在。既然锁都不存在,更不用说这把锁对应的hash结构中的KEY等于ARGV[2]了,所以第一个if条件成立,进入if内部。
接着,跳出函数,锁获取成功。
观察初次加锁的整个过程,我们发现,它和有序集合、队列没有什么关系。
假设名称为anyLock的锁已经被他人持有,现在另一个客户端B尝试加锁。
进入while循环,由于队列是空的,所以lindex获取到的firstThreadId2为空,因此break迅速离开while循环。
检查锁是否存在,此时锁存在啊,所以不能走第一个if逻辑。
检查锁是否被当前线程持有,显然不是,所以不能走第二个if逻辑。
local firstThreadId = redis.call('lindex', KEYS[2], 0); "
尝试着获取队列头元素,显然是不存在的,所以会执行"ttl = redis.call(‘pttl’, KEYS[1]);" 假设ttl=28000。
timeout = ttl + tonumber(ARGV[3]);
timeout = 锁的剩余生存时间 + 当前时间 + 线程等待时间(默认是5000毫秒)
上面这个等式非常重要,举个例子,假设现在是10:00:00,锁的剩余生存时间是20秒,那么timeout = 10:00:25,转换成时间戳。
"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
"redis.call('rpush', KEYS[2], ARGV[2]);" +
"end; " +
执行zadd redisson_lock_timeout:{anyLock} 10:00:25 UUID_02:threadId_02
由于有序集合为空,所以一定能执行成功并返回1。
接着执行rpush redisson_lock_queue:{anyLock} UUID_02:threadId_02
向队列插入一个元素,显然这个元素一定在队头。
如果服务实例B再次加锁,就会通过zadd的逻辑刷新分数(timeout),但zadd返回了0,所以不会对队列有任何影响。
紧接着,另一台服务实例C也来加锁了,此时有一段代码逻辑非常关键:
+ "if timeout <= tonumber(ARGV[4]) then "
+ "redis.call('zrem', KEYS[3], firstThreadId2); "
+ "redis.call('lpop', KEYS[2]); "
while(true)循环会不断的从队列中取出队头元素的名称,接着到有序集合中,查询这个元素对应的timeout,如果timeout小于等于当前时间,则这个元素会从队列和有序集合中移除。但是如果服务C紧挨着服务B发送的请求,可能时间并没有过去多少,那么此时就会退出死循环。
接着就是判断这把锁是否存在,获取到这把锁的人是不是当前服务实例C,显然都不是。
然后判断服务实例C是不是处于队头元素啊?显然也不是啊,处于队头的是服务实例B。所以就会计算锁的ttl,并为服务实例B计算一个timeout,假设当前锁的剩余时间只剩下18000毫秒了,当前时间是10:00:05,那么timeout = 10:00:28,接着就分别向队列和有序集合插入元素,队列是尾入头出,因此服务C一定排在服务B的屁股后面。
有序集合与队列存放了各个服务实例请求获取锁的顺序,在一定的时间范围内,不会因为某个服务实例请求频繁,而让它插队。但是呢,如果某个服务实例长时间没有再次尝试获取锁,则说明它有可能不需要这把锁了,不能让它尸位素餐啊,所以要把它从队列中给移除掉,为排在后面的,真正有需要的服务实例腾出位置。
分数对应着timeout的时间,保证在没有达到这个timeout之前,等待获取锁的队列中,这个客户端请求的相对位置不会被撼动。
答: 使用队列,是为了保存各个服务实例请求获取锁的顺序,队列拥有着头进尾出的天然优势,能够保证先请求获取锁的服务实例,先拿到锁,后请求获取锁的实例,后拿到锁,保证了公平性。使用有序集合,是为了铲除那些占着茅坑不拉屎的服务实例的请求,巩固自己在队列中的位置(不断的刷新timeout,避免出现timeout <= current_time,进而从队列中被移除的悲惨下场)。