Redisson单进程Redis分布式悲观锁的使用与实现
本文基于Redisson 3.7.5
3. 读写锁
Redisson的分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。同时还支持自动过期解锁。该对象允许同时有多个读取锁,但是最多只能有一个写锁。写锁是排它锁,获取写锁的时候不能有已经获取读锁和写锁的,获取写锁后,除了本线程以外没发获取读写锁。
RReadWriteLock rwlock = redisson.getLock("testLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 支持过期解锁功能
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
查看redisson.getLock("testLock");
的源码:
@Override
public RReadWriteLock getReadWriteLock(String name) {
return new RedissonReadWriteLock(connectionManager.getCommandExecutor(), name);
}
可以看出读写锁的实现类是RReadWriteLock,查看RReadWriteLock的源码:
public class RedissonReadWriteLock extends RedissonExpirable implements RReadWriteLock {
public RedissonReadWriteLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
}
/**
* @return 读锁
*/
@Override
public RLock readLock() {
return new RedissonReadLock(commandExecutor, getName());
}
/**
* @return 写锁
*/
@Override
public RLock writeLock() {
return new RedissonWriteLock(commandExecutor, getName());
}
}
3.1. 读写锁实现思路
首先说一下读写锁的特性:
场景1
- 线程A获取了读锁
- 线程B尝试获取读锁,获取成功
- 线程C尝试获取写锁,获取失败
场景2
- 线程A获取了写锁
- 线程B尝试获取读锁,获取失败
- 线程C尝试获取写锁,获取失败
场景3
1. 线程A获取了读锁
2. 线程A尝试获取写锁,获取失败
场景4
1. 线程A获取了写锁
2. 线程A尝试获取读锁,获取成功
场景5
1. 线程A获取了写锁
2. 线程A再次尝试获取写锁,获取成功
3. 线程A尝试获取读锁,获取成功
4. 线程A再次尝试获取读锁,获取成功
5. 线程A释放读锁,线程A还是持有读锁
6. 线程A释放写锁,线程A还是持有写锁
7. 线程A释放写锁,线程A不再持有写锁
8. 线程B尝试获取读锁,获取成功
设计的基于Redis实现的分布式读写锁,需要满足以上五个场景。
看一下在Redis中,读写锁的分布是:
3.2. 读写锁源码分析
3.2.1. 读锁源码
public class RedissonReadLock extends RedissonLock implements RLock {
public RedissonReadLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
}
@Override
String getChannelName() {
return prefixName("redisson_rwlock", getName());
}
String getWriteLockName(long threadId) {
return super.getLockName(threadId) + ":write";
}
String getReadWriteTimeoutNamePrefix(long threadId) {
return suffixName(getName(), getLockName(threadId)) + ":rwlock_timeout";
}
@Override
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//获取当前锁的mode的value
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
//如果mode不存在,证明锁还没被占用,直接获取锁
"if (mode == false) then " +
//设置HASH的mode为read
"redis.call('hset', KEYS[1], 'mode', 'read'); " +
//设置HASH的threadId对应的LockName的value为1(这个1代表重入次数),相当于该thread获取到了锁
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
//设置threadId对应的tReadWriteTimeoutName末尾连着获取锁次数(因为现在是第一次所以是1)这个key为1
"redis.call('set', KEYS[2] .. ':1', 1); " +
//设置两个key的过期时间为锁过期时间
"redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果mode已存在,是read(代表是读锁),或者是write并且获取这个锁的就是本线程
"if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +
//使HASH的threadId对应的LockName的value+1,代表重入锁获取次数加一
"local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
//同时设置threadId对应的tReadWriteTimeoutName末尾连着获取锁次数为1
"local key = KEYS[2] .. ':' .. ind;" +
"redis.call('set', key, 1); " +
//刷新以上两个key的过期时间
"redis.call('pexpire', key, ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end;" +
"return redis.call('pttl', KEYS[1]);",
Arrays.<Object>asList(getName(), getReadWriteTimeoutNamePrefix(threadId)),
internalLockLeaseTime, getLockName(threadId), getWriteLockName(threadId));
}
@Override
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
String keyPrefix = timeoutPrefix.split(":" + getLockName(threadId))[0];
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//获取当前锁的mode的value
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
//如果mode不存在,证明锁已经被释放
"if (mode == false) then " +
//发布释放锁消息
"redis.call('publish', KEYS[2], ARGV[1]); " +
//返回true
"return 1; " +
"end; " +
//如果锁没有释放,并且当前获取锁的并不是本线程,返回null
"local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
"if (lockExists == 0) then " +
"return nil;" +
"end; " +
//如果锁还没有释放,并且是本线程获取的锁,先把对应的计数器(本线程在锁的HASH对应的KEY )减一
"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +
//如果结果为0了,证明重入次数已经归零,可以完全释放锁了,删掉本线程在锁的HASH对应的KEY
"if (counter == 0) then " +
"redis.call('hdel', KEYS[1], ARGV[2]); " +
"end;" +
//删掉threadId对应的tReadWriteTimeoutName末尾连着获取锁次数的key
"redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +
//如果HASH中的key数量大于1(就是除了mode还有其他key),证明还有其他线程获取锁了
"if (redis.call('hlen', KEYS[1]) > 1) then " +
//对于pttl命令来说,结果为-2为不存在,结果为-1代表key存在但是没设置过期时间
//所以设置maxRemainTime初始为-3,因为要返回最大值
"local maxRemainTime = -3; " +
//遍历锁的HASH的每一个key
"local keys = redis.call('hkeys', KEYS[1]); " +
"for n, key in ipairs(keys) do " +
"counter = tonumber(redis.call('hget', KEYS[1], key)); " +
//所有的key除了mode以外,其他key的value都是数字(就是获取锁的次数)
"if type(counter) == 'number' then " +
//锁每获取一次,就会多出一个过期key(对应的tReadWriteTimeout)
//利用次数可以拼接出对应的tReadWriteTimeout从而获取到过期时间
//从次数到最小为1,拼出对应的tReadWriteTimeout,找出最大的过期时间
"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; " +
//如果maxRemainTime大于零,代表有有效的过期时间,证明锁还没被完全释放,返回false
//刷新锁过期时间为maxRemainTime
"if maxRemainTime > 0 then " +
"redis.call('pexpire', KEYS[1], maxRemainTime); " +
"return 0; " +
"end;" +
//如果mode为write代表为写锁,为写锁则不算释放,返回false
"if mode == 'write' then " +
"return 0;" +
"end; " +
"end; " +
//没有其他key,代表锁已经可以被释放,删除这个Lock对应的key,并且发布释放锁的消息
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; ",
Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix),
LockPubSub.unlockMessage, getLockName(threadId));
}
}
3.2.2. 写锁源码:
public class RedissonWriteLock extends RedissonLock implements RLock {
protected RedissonWriteLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
}
@Override
String getChannelName() {
return prefixName("redisson_rwlock", getName());
}
@Override
protected String getLockName(long threadId) {
return super.getLockName(threadId) + ":write";
}
@Override
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//获取当前mode
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
//如果mode不存在,证明锁还没被占用,直接获取
"if (mode == false) then " +
//设置锁的mode为write
"redis.call('hset', KEYS[1], 'mode', 'write'); " +
//设置当前线程占用锁
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
//设置过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
//返回null
"return nil; " +
"end; " +
//如果mode存在且为write
"if (mode == 'write') then " +
//当前占用锁的是本线程
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
//重入,计数加1
"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));
}
@Override
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//检查mode
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
//如果mode不存在则证明锁已释放,发布解锁消息,返回true
"if (mode == false) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
//如果mode为write
"if (mode == 'write') then " +
//验证获取到锁的是否为当前线程
"local lockExists = redis.call('hexists', KEYS[1], ARGV[3]); " +
//如果不为,则返回null
"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]); " +
//返回false
"return 0; " +
"else " +
//如果已经归零,证明锁应该被释放
"redis.call('hdel', KEYS[1], ARGV[3]); " +
//如果HASH的key数量为1,证明只有mode了,直接释放锁
"if (redis.call('hlen', KEYS[1]) == 1) then " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"else " +
//不为1的话,肯定是本线程还获取了读锁,将mode修改为read
"redis.call('hset', KEYS[1], 'mode', 'read'); " +
"end; " +
//返回true
"return 1; "+
"end; " +
"end; " +
"end; "
//返回null
+ "return nil;",
Arrays.<Object>asList(getName(), getChannelName()),
LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
}
3.3. 场景回顾
回顾一下场景
3.3.1. 场景1
- 在时间T1,线程A尝试获取读锁testLock(过期时间为30s)。首先调用
hget testLock mode
检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了读锁,之后redis中的数据结构为:
之后在时间T2,线程B尝试获取读锁(过期时间为30s),先调用
hget testLock mode
检查这个锁的mode,发现mode为read,B也可以获取读锁,获取之后redis中的数据结构为:
之后在时间T3,线程C尝试获取写锁(过期时间为30s),先调用
hget testLock mode
检查这个锁的mode,发现mode为read,获取失败
3.3.2. 场景2
在时间T1,线程A尝试获取写锁testLock(过期时间为30s)。首先调用
hget testLock mode
检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了写锁,之后redis中数据结构为:
在时间T2,线程B尝试获取读锁testLock(过期时间为30s)。先调用
hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的不为自己则获取失败。- 在时间T3,线程C尝试获取写锁testLock(过期时间为30s)。先调用
hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的不为自己则获取失败。
3.3.3. 场景3
- 在时间T1,线程A尝试获取读锁testLock(过期时间为30s)。首先调用
hget testLock mode
检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了读锁,之后redis中的数据结构为:
- 在时间T2,线程A尝试获取写锁testLock(过期时间为30s)。先调用
hget testLock mode
检查这个锁的mode,发现mode为read,直接失败
3.3.4. 场景4
在时间T1,线程A尝试获取写锁testLock(过期时间为30s)。首先调用
hget testLock mode
检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了写锁,之后redis中数据结构为:
在时间T2,线程A尝试获取读锁testLock(过期时间为30s)。先调用
hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
3.3.4. 场景5
在时间T1,线程A尝试获取写锁testLock(过期时间为30s)。首先调用
hget testLock mode
检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了写锁,之后redis中数据结构为:
在时间T2,线程A尝试获取写锁testLock(过期时间为30s)。先调用
hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
在时间T3,线程A尝试获取读锁testLock(过期时间为30s)。首先调用
hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
在时间T4,线程A尝试获取读锁testLock(过期时间为30s)。首先调用
hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
在时间T5,线程A释放读锁。首先调用
hget testLock mode
检查这个锁的mode,发现mode为write,证明锁没有释放,释放一次读锁,整体的过期时间戳是剩下的所有key中过期时间戳最大的,就是T3+30s,之后redis中数据结构为:
在时间T6,线程A释放写锁。首先调用
hget testLock mode
检查这个锁的mode,发现mode为write,证明锁没有释放,释放一次写锁,
刷新所整体过期时间为T6+30s,之后redis中数据结构为:
在时间T7,线程A释放写锁。首先调用
hget testLock mode
检查这个锁的mode,发现mode为write,证明锁没有释放,释放一次写锁,
发现写锁的value已经为0,但是还存在其他除mode以外的key,证明本线程还持有读锁,切换成读锁:
在时间T8,线程B尝试获取读锁(过期时间为30s),先调用
hget testLock mode
检查这个锁的mode,发现mode为read,B也可以获取读锁,获取之后redis中的数据结构为: