持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第29天,点击查看活动详情
读写锁,解决了这样的问题:既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。
在读的地方合理使用读锁,在写的地方合理使用写锁,灵活控制,可以提高程序的执行效率。
Tips: 读本身是线程安全的,加读锁,主要是为了让写锁感知到,在有人读取的时候,不要同时写入。
读写锁的获取规则
在使用读写锁时遵守下面的获取规则:
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功。
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。
- 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。
所以用一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。
总结:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
(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();
}
Tips:Redis 分布式锁,重要看下 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 # 线程名
}