redission实现读写锁的原理
Redisson 实现分布式读写锁的核心原理是 基于 Redis 的 Lua 脚本原子操作 + Pub/Sub 通知机制,在保证强一致性的同时实现高效的读并发(读不阻塞读,写阻塞读)。以下是其核心设计:
一、核心数据结构
Redisson 使用 Redis 的 Hash 结构 存储锁信息:
- Key:
{锁名称}
(如my_lock
) - Hash 字段:
mode
: 锁模式(read
/write
)UUID:threadId
: 持有锁的客户端标识(如c983678b-1421-4c76-8ea0-7f3ab7d9c775:1
)count
: 锁的重入次数(支持可重入)
二、读锁(Read Lock)实现原理
1. 获取读锁流程
-- Lua 脚本原子执行
if (redis.call('exists', KEYS[1]) == 0) then -- 无任何锁redis.call('hset', KEYS[1], ARGV[2], 1); -- 创建读锁redis.call('pexpire', KEYS[1], ARGV[1]); -- 设置超时return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 当前线程已持有读锁redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 重入次数+1redis.call('pexpire', KEYS[1], ARGV[1]); -- 刷新超时return nil;
end;
if (redis.call('hexists', KEYS[1], 'mode') == 1) and (redis.call('hget', KEYS[1], 'mode') == 'read') then -- 已有其他读锁redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 直接叠加读锁计数redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;
return redis.call('pttl', KEYS[1]); -- 存在写锁,返回剩余时间(需等待)
关键点:
- 只要当前无写锁(
mode
非write
),读锁可直接获取,不阻塞其他读锁。 - 多个读锁共享同一个 Hash 结构,通过字段区分不同客户端。
2. 读锁释放
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then return nil; end; -- 锁不存在
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); -- 重入次数-1
if (counter == 0) thenredis.call('hdel', KEYS[1], ARGV[2]); -- 移除当前线程的锁
end;
if (redis.call('hlen', KEYS[1]) == 1) then -- 只剩 mode 字段(无任何锁)redis.call('del', KEYS[1]); -- 删除整个 Keyredis.call('publish', KEYS[2], ARGV[1]); -- 发布解锁通知
end;
return 1;
三、写锁(Write Lock)实现原理
1. 获取写锁流程
if (redis.call('exists', KEYS[1]) == 0) 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 (redis.call('hexists', KEYS[1], 'mode') == 1) and (redis.call('hget', KEYS[1], 'mode') == 'write') then -- 已有写锁if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 当前线程持有写锁redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 重入次数+1redis.call('pexpire', KEYS[1], ARGV[1]);return nil;end;
end;
return redis.call('pttl', KEYS[1]); -- 存在读锁或其他写锁,返回剩余时间(需等待)
关键点:
- 写锁要求绝对互斥:必须无任何锁(读/写)存在才能获取。
- 若存在读锁或其他写锁,客户端需等待(通过 Pub/Sub 监听解锁通知)。
2. 写锁释放
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; -- 锁不存在
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); -- 重入次数-1
if (counter == 0) thenredis.call('hdel', KEYS[1], ARGV[3]); -- 移除持有者
end;
if (redis.call('hlen', KEYS[1]) == 1) then -- 只剩 mode 字段redis.call('del', KEYS[1]); -- 删除 Keyredis.call('publish', KEYS[2], ARGV[1]); -- 发布解锁通知
end;
return 1;
四、阻塞等待与通知机制
1. 锁竞争时的等待策略
- 当锁获取失败时,Redisson 不轮询,而是通过 Redis 的 Pub/Sub 订阅锁释放事件:
// 伪代码:订阅解锁通知 RedisPubSub listener = new RedisPubSub() {void onMessage(String channel, String message) {if (message.equals("unlock_msg")) {tryAcquireLock(); // 收到通知后重新尝试获取锁}} }; redis.subscribe(listener, "lock_channel");
- 优势:避免频繁轮询 Redis,减少网络开销。
2. 锁超时与续期
- 看门狗机制(Watchdog):
后台线程每隔 10 秒检查锁是否仍被持有,若持有则刷新 TTL(默认 30 秒),防止业务未完成时锁过期。if (lockAcquired) {scheduleExpirationRenewal(threadId); // 启动看门狗线程 }
五、公平锁实现
Redisson 还提供公平读写锁(按请求顺序获取锁):
- 使用 Redis List 结构作为请求队列。
- 每个客户端获取锁前在队列尾部追加自己的请求 ID。
- 只有队首的请求有权尝试获取锁,避免饥饿问题。
总结:Redisson 读写锁的核心优势
- 读读并发:通过 Hash 结构叠加读锁计数,无写锁时读操作永不阻塞。
- 原子性:所有锁操作通过 Lua 脚本在 Redis 单线程中执行,无竞态条件。
- 低开销等待:基于 Pub/Sub 的事件通知取代轮询。
- 容错性:锁超时自动释放 + 看门狗续期,避免死锁。
- 可重入:支持同一线程多次加锁(通过
count
字段实现)。
注:实际代码比上述伪代码更复杂(含重试机制、异常处理等),但核心逻辑一致。建议直接阅读 Redisson 源码 中的
RedissonReadLock
和RedissonWriteLock
类。