分布式锁:Redisson源码解析-ReadWriteLock

571 阅读8分钟

说到读写锁,大家都会很迅速的反应过来,读写锁的存在就是为了提升实际的应用的并发能力,可以保证读读不互斥,读写互斥,写写互斥

一、概念及实现

1. 概念

官方文档

  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();
  • 使用起来还是很简单的

初始化

  1. 实际初始化的对象是一个RedissonReadWriteLock,这个对象会持有两个小的对象RedissonReadLock RedissonWriteLock
  2. 这样在使用的时候,就可以根据需要来控制需要的是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; 
  1. 从redis中获取hash结构数据lockName的mode属性值
    • 第一次来加锁,那么这个key都不存在,那么mode属性肯定也是不存在了
  2. 设置lockName hash结构key,且mode属性值为read
{
  "lockName": {
    "mode": "read"
  }
}
  1. 设置lockName hash结构key的uuid:threadId的属性值为1
{
  "lockName": {
    "mode": "read",
    "uuid:threadId": 1
  }
}
  1. 设置{lockName}:uuid:threadId:rwlock_timeout:1的一个key值为1
{
  "lockName": {
    "mode": "read",
    "uuid:threadId": 1
  },
  
  "{lockName}:uuid:threadId:rwlock_timeout:1": 1
}
  1. 设置{lockName}:uuid:threadId:rwlock_timeout:1 key的失效时间为leaseTime
  2. 设置lockName key的失效时间为leaseTime
  1. 返回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进行续约

  1. 获取lockNamekey中uuid:threadId的值,这个值表示的是这个线程获取的读锁数量,在第一次加读锁后,这个地方的值应该是1
  2. 如果当前线程还在持有读锁,就会走watchdog的逻辑了
  3. 给lockName的key续约,设置过期时间为leaseTime
  4. 获取lockName key中的所有的属性,并遍历
  5. 值为数字的key属性进行处理
  6. 会遍历给 {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]);

在加读锁的代码中,还有下面一个分支判断

如果资源已经被加了读锁,或者是被当前线程加了写锁

  1. 自增一下lockName的属性uuid:threadId值,表示加锁的数量+1
  2. 设置key为 {lockName}:uuid:threadId:rwlock_timeout:i值为1
  3. 设置{lockName}:uuid:threadId:rwlock_timeout:i的过期时间为leaseTime
  4. 获取lockName的ttl,设置lockName的过期时间为 ttl和leaseTime之间的更大值,所以有可能存在ttl是比leaseTime大的情况,通常处于加了可重入写锁
  5. 如果加锁成功就返回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;
  1. 先去redis中获取lockName的mode属性
  2. 如果没有加过锁,那么这个mode就是不存在的
  3. 设置lockName key属性mode的值为write
{
  "lockName": {
    "mode": "write"
  }
}
  1. 设置lockName一个属性uuid:threadId:write值为1
{
  "lockName": {
    "mode": "write",
    "uuid:threadId:write": 1
  }
}
  1. 设置lockName的过期时间为leaseTime
  2. 如果加锁成功就返回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]);

同一线程来加写锁,表明这个是可重入写锁

  1. 获取lockName的mode属性,如果等于write就是已经处于写锁获取的状态了,再通过判断lockName中的属性uuid:threadId:write是否存在来判断是否是可重入锁
  2. 会先将lockName的属性uuid:threadId:write的值+1
  3. 获取lockName的ttl
  4. 设置lockName的过期时间为:ttl+leaseTime,时间叠加
  5. 如果加锁成功就返回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;
  1. 从redis中获取lockName的mode属性,如果不存在就表示已经没有人持有锁了,直接返回1
  2. 判断是否存在lockName中是否存在uuid:threadId属性,不存在直接返回null
  3. 将lockName中的uuid:threadId属性值-1,如果此时发现属性值已经为0了,就直接删除掉uuid:threadId的属性
  4. 删除对应的另一个key:{lockName}:uuid:threadId:rwlock_timeout:(counter+1)
  5. 获取lockName里面所有的属性,获取keys--> {lockName}:uuid:threadId:rwlock_timeout:i中的最大ttl
  6. 设置lockName的过期时间为最大的ttl,返回0,表示释放锁成功
  7. 如果已经被写锁持有了,就返回0,表示释放锁成功
  8. 其他情况将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;

// 当前加锁为writeif (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;
        

三、思考

读锁

  1. 加读锁的时候,实际是产出了两个redis key,一个是lockName,一个是{lockName}:uuid:threadId:rw_timeout:1的key,同时如果有其他线程再来加读锁的话,会持续递增这个数据

写锁

  1. 加写锁就是一个锁,但是他的属性值变了,是uuid:threadId:write,但是同时,可能被当前线程获取到读锁