【Redisson】读写锁 锁源码剖析(一)

452 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第29天,点击查看活动详情

读写锁,解决了这样的问题:既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。

在读的地方合理使用读锁,在写的地方合理使用写锁,灵活控制,可以提高程序的执行效率。

Tips:  读本身是线程安全的,加读锁,主要是为了让写锁感知到,在有人读取的时候,不要同时写入。

读写锁的获取规则

在使用读写锁时遵守下面的获取规则:

  1. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功。
  2. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。
  3. 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。

所以用一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。

总结:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)

(1)源码剖析 - 加锁

举个栗子:

@Test
public void test() {
    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rwLock");
​
    RLock readLock = readWriteLock.readLock();
    RLock writeLock = readWriteLock.writeLock();
​
    readLock.lock();
    writeLock.lock();
​
    readLock.unlock();
    writeLock.unlock();
}

TipsRedis 分布式锁,重要看下 lua 脚本里的实现。

1)读锁 - 加锁

加锁对应源码位于: RedissonReadLock#tryLockInnerAsync

对应参数如下:

  • KEYS[1]:锁的名称 "rwLock"

  • KEYS[2]:锁超时 key{锁名}:UUID:threadId:rwlock_timeout

    {readWriteLock}:3411bcf2-dc6c-45c7-b183-352e4448368b:1:rwlock_timeout

  • ARGV[1]:锁时间,默认 30秒

  • ARGV[2]:线程key 名称,UUID:threadId

  • ARGV[3]:写锁的名称:UUID:threadId:write

    写锁:3411bcf2-dc6c-45c7-b183-352e4448368b:1:write

-- 获取锁的模式
local mode = redis.call('hget', KEYS[1], 'mode'); 
if (mode == false) then  -- 锁模式不存在,即锁不存在
    redis.call('hset', KEYS[1], 'mode', 'read'); 
    -- 设置值:3411bcf2-dc6c-45c7-b183-352e4448368b:1 的值为 1
    redis.call('hset', KEYS[1], ARGV[2], 1); 
    -- 设置值:rwLock:3411bcf2-dc6c-45c7-b183-352e4448368b:1:rwlock_timeout:1  的值为 1
    redis.call('set', KEYS[2] .. ':1', 1);           -- .. 是拼接的意思
    redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
end; 
​
-- 如果模式是读锁 或者 (模式是写锁 且当前 3411bcf2-dc6c-45c7-b183-352e4448368b:1:write 的值为 1)
if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then 
    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]);

Redis 中结构如下:

# 读锁加锁的数据结构
"rwLock" {
   "mode": "read",  # 模式
   "3411bcf2-dc6c-45c7-b183-352e4448368b:1": 1  # 线程名
}
rwLock:3411bcf2-dc6c-45c7-b183-352e4448368b:1:rwlock_timeout:1 = 1

2)写锁 - 加锁

加锁对应源码位于: RedissonWriteLock#tryLockInnerAsync

对应参数如下:

  • KEYS[1]:锁的名称 "rwLock"
  • ARGV[1]:锁时间,默认 30秒
  • ARGV[2]:线程锁名key 名称,UUID:threadId:write
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);               -- 写锁的重入次数 +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]);

Redis 中结构如下:

# 读锁加锁的数据结构
"rwLock" {
   "mode": "write",  # 模式
   "3411bcf2-dc6c-45c7-b183-352e4448368b:1": 1  # 线程名
}