前言
读锁是共享的,写锁是排他的。
读锁的实现RedissonReadLock,继承自非公平锁RedissonLock,主要重写了下面几个方法:
- tryLockInnerAsync:获取锁
- unlockInnerAsync:手动解锁
- renewExpirationAsync:重置过期时间
- forceUnlockAsync:强制解锁
- isLocked:锁是否被持有
下面逐个分析
获取锁
简介:如果锁不存在,则直接持有;如果锁为读模式,或者当前线程持有写锁,则当前线程可以直接成功获取锁。
锁的信息和非公平锁一样,存放在hash结构中,包含属性:mode【读或者写】,
@Override
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('hset', KEYS[1], 'mode', 'read'); " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('set', KEYS[2] .. ':1', 1); " +
"redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +
"local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"local key = KEYS[2] .. ':' .. ind;" +
"redis.call('set', key, 1); " +
"redis.call('pexpire', key, ARGV[1]); " +
"local remainTime = redis.call('pttl', KEYS[1]); " +
"redis.call('pexpire', KEYS[1], math.max(remainTime, ARGV[1])); " +
"return nil; " +
"end;" +
"return redis.call('pttl', KEYS[1]);",
Arrays.<Object>asList(getName(), getReadWriteTimeoutNamePrefix(threadId)),
internalLockLeaseTime, getLockName(threadId), getWriteLockName(threadId));
}
执行步骤:
- 获取当前锁的模式【mode】,如果不存在,则拿锁成功,写入当前客户端线程锁信息:mode,持有锁次数,设置锁过期时间;
- 为当前客户端线程设置一个缓存,和当前获取的锁过期时间一致;返回true;
- 如果mode是读,或者【mode是写,并且持有写锁的线程是当前客户端】,则累加锁的持有次数,用当前线程id和持有次数组合成一个key,设定过期时间;
- 获取当前key的剩余过期时间,取剩余时间和方法参数中过期时间比较大的一个值,重置缓存过期时间;
- 如果前面两个分支都不满足条件,则返回过期时间;
解锁
手动解锁
简介:
@Override
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
"if (lockExists == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +
"if (counter == 0) then " +
"redis.call('hdel', KEYS[1], ARGV[2]); " +
"end;" +
"redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +
"if (redis.call('hlen', KEYS[1]) > 1) then " +
"local maxRemainTime = -3; " +
"local keys = redis.call('hkeys', KEYS[1]); " +
"for n, key in ipairs(keys) do " +
"counter = tonumber(redis.call('hget', KEYS[1], key)); " +
"if type(counter) == 'number' then " +
"for i=counter, 1, -1 do " +
"local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i); " +
"maxRemainTime = math.max(remainTime, maxRemainTime);" +
"end; " +
"end; " +
"end; " +
"if maxRemainTime > 0 then " +
"redis.call('pexpire', KEYS[1], maxRemainTime); " +
"return 0; " +
"end;" +
"if mode == 'write' then " +
"return 0;" +
"end; " +
"end; " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; ",
Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix),
LockPubSub.UNLOCK_MESSAGE, getLockName(threadId));
}
执行步骤:
- 获取锁的模式,如果锁不存在,则通知其他客户端线程进行尝试拿锁;如果锁存在,执行后续逻辑;
- 判断持有锁的是否为当前线程,如果不是,则直接返回false;
- 读取当前线程减一之后剩余的持有次数,如果剩余次数为零,则删除当前线程的锁持有凭证;
- 删除锁超时组合key,当前线程id + 持有次数;redisson读锁对每一次持有都记录超时时间;
- 如果资源锁中的属性大于一【表示有其他线程持有当前资源锁】,定义锁衰减时间,获取锁hash中所有的键值对【持有当前锁的客户端线程】,遍历;如果有持有次数,则取出每次锁的过期时间,通过计算,获取最大的过期时间;
- 如果最大过期时间大于零,则设置此锁资源的过期时间为此值,并返回失败;如果小于零,则继续往下执行;
- 如果锁模式为写,则返回解锁失败;跳出【第5步】的IF分支,继续往下执行;解锁失败是因为资源锁中的属性大于一,且模式为write,表示还有其他线程持有了当前资源的写锁;写锁是不可共享的,说明当前客户端线程是读锁,不能释放资源的独占锁。
- 如果资源锁HASH中没有其他客户端线程持有锁,则删除锁资源,并发布锁释放的信号,通知其他排队线程进行争抢,返回成功;
强制解锁
简介:强制解锁,只能是读锁的模式下,如果当前锁模式为写锁,则强制解锁失败。
@Override
public RFuture<Boolean> forceUnlockAsync() {
cancelExpirationRenewal(null);
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hget', KEYS[1], 'mode') == 'read') then " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0; ",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE);
}
执行步骤:
- 判断当前锁资源是否为读锁模式,如果是,则删除锁资源,并发布锁释放的信号,通知其他排队线程进行争抢,返回成功;
- 如果当前锁资源是写锁模式,则直接返回失败。
这里有个疑问:如果模式为写锁,而且执行强制释放的是持有的客户端线程,也不能强制释放吗?毕竟持有者应该是有资格进行锁释放的。
猜测:强制解锁是没有参数传入的,不知道执行者的身份;所以不能判定当前写锁的持有者是否为当前客户端线程;
重置过期时间
执行的契机是当前客户端线程成功获取锁之后,重置其超时时间为最初调用tryLock(timeout)方法时,传入的timeout值【默认为看门狗时间:30秒】。
@Override
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"local counter = redis.call('hget', KEYS[1], ARGV[2]); " +
"if (counter ~= false) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"if (redis.call('hlen', KEYS[1]) > 1) then " +
"local keys = redis.call('hkeys', KEYS[1]); " +
"for n, key in ipairs(keys) do " +
"counter = tonumber(redis.call('hget', KEYS[1], key)); " +
"if type(counter) == 'number' then " +
"for i=counter, 1, -1 do " +
"redis.call('pexpire', KEYS[2] .. ':' .. key .. ':rwlock_timeout:' .. i, ARGV[1]); " +
"end; " +
"end; " +
"end; " +
"end; " +
"return 1; " +
"end; " +
"return 0;",
Arrays.<Object>asList(getName(), keyPrefix),
internalLockLeaseTime, getLockName(threadId));
}
执行步骤:
- 获取当前线程持有锁的次数,如果没有持有,则直接返回false;
- 重置资源锁的超时时间为当前客户端调用tryLock(waitTime,leaseTime)方法时,传入的leaseTime值;问题:读锁是共享的,资源锁的超时时间会被最后一个持有的客户端线程重置,线程安全吗,合理吗?
- 如果资源所被多个客户端线程持有,则更新每个客户端每次持有记录的超时时间为leaseTime的值。
是否被持有
这个方法也被重写,在非公平锁中,只用判断锁资源在redis中是否存在【isExists()】。
@Override
public boolean isLocked() {
RFuture<String> future = commandExecutor.writeAsync(getName(), StringCodec.INSTANCE, RedisCommands.HGET, getName(), "mode");
String res = get(future);
return "read".equals(res);
}
执行逻辑:获取资源锁的模式,判断模式是否为【read】。