前言
读锁是共享的,写锁是排他的,使用与读多写少的业务场景。
写锁的实现【RedissonWriteLock】,继承自非公平锁RedissonLock,主要重写了下面几个方法:
- tryLockInnerAsync:获取锁
- unlockInnerAsync:手动解锁
- forceUnlockAsync:强制解锁
- isLocked:锁是否被持有
可以发现写锁比读锁少重写一个方法:renewExpirationAsync,直接使用了父类非公平锁内的默认实现。
获取锁
@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', 'write'); " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (mode == 'write') then " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"local currentExpire = redis.call('pttl', KEYS[1]); " +
"redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]); " +
"return nil; " +
"end; " +
"end;" +
"return redis.call('pttl', KEYS[1]);",
Arrays.<Object>asList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
执行步骤:
- 如果锁没有被其他客户端持有,则当前客户端线程直接持有锁,并设置mode为write;
- 如果资源锁已经被其他客户端持有【写锁】,且持有者不是当前线程,则返回等待时间,获取锁失败;
- 如果持有写锁的是当前线程,则累加持有次数,并延长锁超时时间;超时时间根据重入时带上的超时参数累加所得。
疑问:源码中不判断当前持有写锁的是否为当前线程,如果一个线程先持有读锁,再重入获取写锁,是否会造成写锁被共享的异常情况?
释放锁
手动释放
手动释放的逻辑比读锁简单很多,因为读锁被设定为共享的,释放时,需要考虑其他持有锁的线程,根据其中超时时间最长的一个客户端,来重置锁资源的超时时间。
@Override
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
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;" +
"if (mode == 'write') then " +
"local lockExists = redis.call('hexists', KEYS[1], ARGV[3]); " +
"if (lockExists == 0) then " +
"return nil;" +
"else " +
"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('hdel', KEYS[1], ARGV[3]); " +
"if (redis.call('hlen', KEYS[1]) == 1) then " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"else " +
// has unlocked read-locks
"redis.call('hset', KEYS[1], 'mode', 'read'); " +
"end; " +
"return 1; "+
"end; " +
"end; " +
"end; "
+ "return nil;",
Arrays.<Object>asList(getName(), getChannelName()),
LockPubSub.READ_UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
执行步骤:
- 判断锁是否存在,如果不存在,则直接发送释放信号,通知其他线程争抢,返回true;
- 如果锁的模式不是write,直接返回false;
- 如果锁的模式是write,判断当前线程是否在持有列表中,如果当前线程没有持有记录,则直接返回false;否则继续往下执行;
- 递减当前线程的持有次数,如果持有次数不为零,则重置锁的过期时间,返回false;
- 如果当前线程递减次数之后为零,则删除客户端线程的持有记录;
- 如果此时资源锁中没有其他持有者,则删除资源锁,并通知其他线程争抢,否则将资源锁的mode切换为read,返回true;
疑问:与获取中的问题一样,释放时不能确定写锁持有者是否为当前线程,是否会误操作?
强制释放
强制释放逻辑和读锁中的基本一致,读锁中判断模式是否为read,这里判断是否为write,如果是write,表示锁已经被持有,且为写锁。操作逻辑比较简单,直接删除锁资源,并发布锁释放的消息,提示其他客户端线程订阅者进行争抢锁。
@Override
public RFuture<Boolean> forceUnlockAsync() {
cancelExpirationRenewal(null);
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hget', KEYS[1], 'mode') == 'write') then " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0; ",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.READ_UNLOCK_MESSAGE);
}
是否被持有
这个方法也被重写,在非公平锁中,只用判断锁资源在redis中是否存在。
@Override
public boolean isLocked() {
RFuture<String> future = commandExecutor.writeAsync(getName(), StringCodec.INSTANCE, RedisCommands.HGET, getName(), "mode");
String res = get(future);
return "write".equals(res);
}
执行逻辑:获取资源锁的模式,判断模式是否为【write】。