本文基于Redisson 3.7.5
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());
}
}
首先说一下读写锁的特性:
场景1
场景2
场景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中,读写锁的分布是:
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
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand 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.:" + 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.
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
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand 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.
回顾一下场景
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,获取失败
在时间T1,线程A尝试获取写锁testLock(过期时间为30s)。首先调用hget testLock mode
检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了写锁,之后redis中数据结构为:
在时间T2,线程B尝试获取读锁testLock(过期时间为30s)。先调用hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的不为自己则获取失败。
hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的不为自己则获取失败。hget testLock mode
检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了读锁,之后redis中的数据结构为:hget testLock mode
检查这个锁的mode,发现mode为read,直接失败在时间T1,线程A尝试获取写锁testLock(过期时间为30s)。首先调用hget testLock mode
检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了写锁,之后redis中数据结构为:
在时间T2,线程A尝试获取读锁testLock(过期时间为30s)。先调用hget testLock mode
检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
在时间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中的数据结构为: