说到读写锁,大家都会很迅速的反应过来,读写锁的存在就是为了提升实际的应用的并发能力,可以保证读读不互斥,读写互斥,写写互斥
一、概念及实现
1. 概念
官方文档
- Github
- 核心接口ReadWriteLock是基于Java里的ReadWriteLock构建的,读锁和写锁都实现了 RLock 接口
- 允许多个 ReadLock 所有者和仅一个 WriteLock 所有者
- 就是读读不互斥
- 写写互斥
- 读写互斥
- 如果获取锁的 Redisson 实例崩溃,那么这种锁可能会永远挂在获取状态
- 为了避免这种Redisson维护锁看门狗,它会在锁持有者Redisson实例存活时延长锁到期时间
- 也可以设置锁的持有时间leaseTime
2. 实现
RReadWriteLock lock = redissonClient.getReadWriteLock("lockName");
lock.readLock().lock();
lock.readLock().unlock();
lock.writeLock().lock();
lock.writeLock().unlock();
- 使用起来还是很简单的
初始化
- 实际初始化的对象是一个
RedissonReadWriteLock
,这个对象会持有两个小的对象RedissonReadLock
RedissonWriteLock
- 这样在使用的时候,就可以根据需要来控制需要的是writeLock还是readLock
二、源码解析
话不多说,直接看源码说明
加锁
1. 加读锁
实际走一遍lock.readLock().lock()
流程的时候,会发现,基本整个代码的逻辑都是走的RLock的,而重构了实际加锁的脚本tryLockInnerAsync
加读锁lua逻辑
// KEY[1] = lockName
// KEY[2] = {lockName}:uuid:threadId:rwlock_timeout
// ARVG[1] = leaseTime
// ARVG[2] = uuid:threadId
// ARVG[3] = uuid:threadId:write
// hget lockName mode
local mode = redis.call('hget', KEYS[1], 'mode');
// mode 属性不存在
if (mode == false) then
// 设置lockName mode属性值为read,直接加读锁
// hset lockName mode read
redis.call('hset', KEYS[1], 'mode', 'read');
// 加锁数量增加1
// hset lockName uuid:threadId 1
redis.call('hset', KEYS[1], ARGV[2], 1);
// set {lockName}:uuid:threadId:rwlock_timeout:1 1
redis.call('set', KEYS[2] .. ':1', 1);
// pexpire {lockName}:uuid:threadId:rwlock_timeout:1 leaseTime
redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]);
// 设置过期时间
// pexpire lockName leaseTime
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
- 从redis中获取hash结构数据lockName的mode属性值
- 第一次来加锁,那么这个key都不存在,那么mode属性肯定也是不存在了
- 设置lockName hash结构key,且mode属性值为read
{
"lockName": {
"mode": "read"
}
}
- 设置lockName hash结构key的uuid:threadId的属性值为1
{
"lockName": {
"mode": "read",
"uuid:threadId": 1
}
}
- 设置
{lockName}:uuid:threadId:rwlock_timeout:1
的一个key值为1
{
"lockName": {
"mode": "read",
"uuid:threadId": 1
},
"{lockName}:uuid:threadId:rwlock_timeout:1": 1
}
- 设置
{lockName}:uuid:threadId:rwlock_timeout:1
key的失效时间为leaseTime - 设置
lockName
key的失效时间为leaseTime - 返回
null
所以,如果加了一个读锁,那么就会生成两个key, lockName
{lockName}:uuid:threadId:rwlock_timeout:1
2. 读锁watchdog
加锁成功后,如果没有设置了leaseTime,就会执行调度续约时间修正,发现加读锁后的watchdog被重写了, org.redisson.RedissonReadLock#renewExpirationAsync
// KEY[1] = lockName
// KEY[2] = {lockName}
// ARVG[1] = leaseTime
// ARVG[2] = uuid:threadId
// hget lockName uuid:threadId
local counter = redis.call('hget', KEYS[1], ARGV[2]);
if (counter ~= false) then
// pexpire lockName leaseTime
redis.call('pexpire', KEYS[1], ARGV[1]);
// hlen lockName > 1
if (redis.call('hlen', KEYS[1]) > 1) then
// hkeys lockName
local keys = redis.call('hkeys', KEYS[1]);
for n, key in ipairs(keys) do
// hget lockName key
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;
return 1;
end;
end;
return 0;
对redis的lock进行续约
- 获取
lockName
key中uuid:threadId
的值,这个值表示的是这个线程获取的读锁数量,在第一次加读锁后,这个地方的值应该是1 - 如果当前线程还在持有读锁,就会走watchdog的逻辑了
- 给lockName的key续约,设置过期时间为leaseTime
- 获取lockName key中的所有的属性,并遍历
- 值为数字的key属性进行处理
- 会遍历给
{lockName}:uuid:threadId:rwlock_timeout:i
续约leaseTime,续约成功则返回1
3. 加可重入读锁
在加锁的状态下,现在同一线程又来加读锁了,也就是可重入锁
// KEY[1] = lockName
// KEY[2] = {lockName}:uuid:threadId:rwlock_timeout
// ARVG[1] = leaseTime
// ARVG[2] = uuid:threadId
// ARVG[3] = uuid:threadId:write
// 如果已经是加了读锁的了或者是加写锁的人是自己
if (mode == 'read')
// 或者是 加了写锁且lockName的uuid:threadId:write属性是存在的
or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then
加锁数量增加1
// incrby lockName uuid:threadId 1
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]);
在加读锁的代码中,还有下面一个分支判断
如果资源已经被加了读锁,或者是被当前线程加了写锁
- 自增一下lockName的属性uuid:threadId值,表示加锁的数量+1
- 设置key为
{lockName}:uuid:threadId:rwlock_timeout:i
值为1 - 设置
{lockName}:uuid:threadId:rwlock_timeout:i
的过期时间为leaseTime - 获取lockName的ttl,设置lockName的过期时间为 ttl和leaseTime之间的更大值,所以有可能存在ttl是比leaseTime大的情况,通常处于加了可重入写锁
- 如果加锁成功就返回null
4. 加写锁
写锁也是一样的,在redis的数据结构上,尝试加了写锁
// KEY[1] lockName
// ARGV[1] leaseTime
// ARGV[2] uuid:threadId:write
// hget lockName mode
local mode = redis.call('hget', KEYS[1], 'mode');
if (mode == false) then
// hset lockName mode write
redis.call('hset', KEYS[1], 'mode', 'write');
// hset lockName uuid:threadId:write 1
redis.call('hset', KEYS[1], ARGV[2], 1);
// pexpire lockName leaseTime
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
- 先去redis中获取lockName的mode属性
- 如果没有加过锁,那么这个mode就是不存在的
- 设置lockName key属性mode的值为write
{
"lockName": {
"mode": "write"
}
}
- 设置lockName一个属性
uuid:threadId:write
值为1
{
"lockName": {
"mode": "write",
"uuid:threadId:write": 1
}
}
- 设置lockName的过期时间为leaseTime
- 如果加锁成功就返回null
5. 写锁watchdog
- 判断lockName中存在属性uuid:threadId
- 设置过期时间为leaseTime
6. 可重入加写锁
// KEY[1] lockName
// ARGV[1] leaseTime
// ARGV[2] uuid:threadId:write
if (mode == 'write') then
// hexists lockName uuid:threadId:write
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
// hincrby lockName uuid:threadId:write 1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
// pttl lockName
local currentExpire = redis.call('pttl', KEYS[1]);
// pexpire lockName ttl+leaseTime
redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]);
return nil;
end;
end;
return redis.call('pttl', KEYS[1]);
同一线程来加写锁,表明这个是可重入写锁
- 获取lockName的mode属性,如果等于write就是已经处于写锁获取的状态了,再通过判断lockName中的属性
uuid:threadId:write
是否存在来判断是否是可重入锁 - 会先将lockName的属性
uuid:threadId:write
的值+1 - 获取lockName的ttl
- 设置lockName的过期时间为:ttl+leaseTime,时间叠加
- 如果加锁成功就返回null
7. 其他线程加写锁
加写锁的lua逻辑里面,是有两个判断的,一个是判断是否lockName被加写锁、一个是加锁的线程是当前线程才会走到对应的逻辑里,否则就会直接返回lockname的ttl
8. 可重入加读锁
与上面的3的逻辑是一致的
9. 加读锁
加读锁的时候也会判断,如果没有加锁,就会直接加读锁,如果加锁了,就会判断锁是不是读锁,或者加写锁的是自己的线程
10. 加了读锁之后,加写锁
毫无波澜,直接返回了ttl
释放锁
1. 释放读锁
// KEY[1] lockName
// KEY[2] redisson_rwlock:{lockName}
// KEY[3] {lockName}:uuid:threadId:rwlock_timeout
// KEY[4] {lockName}
// ARVG[1] 0
// ARVG[2] uuid:threadId
// hget lockName mode
local mode = redis.call('hget', KEYS[1], 'mode');
if (mode == false) then
// publish redisson_rwlock:{lockName} 0
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
// hexists lockName uuid:threadId
local lockExists = redis.call('hexists', KEYS[1], ARGV[2]);
if (lockExists == 0) then
return nil;
end;
// hincrby lockName uuid:threadId -1
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter == 0) then
// hdel lockName uuid:threadId
redis.call('hdel', KEYS[1], ARGV[2]);
end;
// del {lockName}:uuid:threadId:rwlock_timeout:(counter+1)
redis.call('del', KEYS[3] .. ':' .. (counter+1));
// hlen lockName > 1
if (redis.call('hlen', KEYS[1]) > 1) then
local maxRemainTime = -3;
// hkeys lockName
local keys = redis.call('hkeys', KEYS[1]);
for n, key in ipairs(keys) do
// hget lockName keys
counter = tonumber(redis.call('hget', KEYS[1], key));
if type(counter) == 'number' then
// 遍历获取最大的ttl
for i=counter, 1, -1 do
// pttl {lockName}:key:rwlock_timeout:i
local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i);
//
maxRemainTime = math.max(remainTime, maxRemainTime);
end;
end;
end;
if maxRemainTime > 0 then
// pexpire lockName maxRemainTime
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;
- 从redis中获取lockName的mode属性,如果不存在就表示已经没有人持有锁了,直接返回1
- 判断是否存在lockName中是否存在uuid:threadId属性,不存在直接返回null
- 将lockName中的uuid:threadId属性值-1,如果此时发现属性值已经为0了,就直接删除掉uuid:threadId的属性
- 删除对应的另一个key:
{lockName}:uuid:threadId:rwlock_timeout:(counter+1)
- 获取lockName里面所有的属性,获取keys–>
{lockName}:uuid:threadId:rwlock_timeout:i
中的最大ttl - 设置lockName的过期时间为最大的ttl,返回0,表示释放锁成功
- 如果已经被写锁持有了,就返回0,表示释放锁成功
- 其他情况将lockName key删除掉
2. 释放写锁
// KEY[1] = lockName
// KEY[2] = redisson_rwlock:lockName
// ARVG[1] = 0 // LockPubSub.READ_UNLOCK_MESSAGE
// ARVG[2] = leaseTime
// ARVG[3] = uuid:threadId:write
// 获取并校验mode
local mode = redis.call('hget', KEYS[1], 'mode');
if (mode == false) then
// 不存在,直接返回1
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
// 当前加锁为write锁
if (mode == 'write') then
// 判断是否存在当前线程加的写锁 uuid:threadId:write
local lockExists = redis.call('hexists', KEYS[1], ARGV[3]);
if (lockExists == 0) then
// 不存在直接返回null,这里的意思就是不是自己加的写锁,不能释放
return nil;
else
// uuid:threadId:write值-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
// 如果有可重入锁,就重置一下过期时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
// 删除uuid:threadId:write属性
redis.call('hdel', KEYS[1], ARGV[3]);
// 判断了一下是不是只有一个写锁持有
if (redis.call('hlen', KEYS[1]) == 1) then
// 删除key
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
else
// 表示有读锁,就转成读锁mode
redis.call('hset', KEYS[1], 'mode', 'read');
end;
return 1;
end;
end;
end;
return nil;
三、思考
读锁
- 加读锁的时候,实际是产出了两个redis key,一个是lockName,一个是{lockName}:uuid:threadId:rw_timeout:1的key,同时如果有其他线程再来加读锁的话,会持续递增这个数据
写锁
- 加写锁就是一个锁,但是他的属性值变了,是uuid:threadId:write,但是同时,可能被当前线程获取到读锁