知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方请评论,我们一起交流!
Redisson 通过 客户端唯一标识(UUID + 线程ID) 和 Lua 脚本的原子性验证 确保只有加锁的线程才能解锁,从而避免锁被误释放。以下是其核心实现原理和关键步骤的深度分析:
一、加锁时的线程标识
1. 生成唯一锁标识
Redisson 在加锁时为每个客户端生成全局唯一的标识,包含两部分:
- UUID:客户端启动时生成,确保不同客户端之间的唯一性。
- 线程ID :当前线程的 ID(
Thread.currentThread().getId()
),确保同一客户端不同线程的唯一性。
最终标识格式 :
UUID:threadId
(如 4a9d6c80-1a2b-3c4d-5e6f-7a8b9c0d1e2f:1
)
2. 存储标识到 Redis
加锁时,Redisson 执行以下 Lua 脚本(以 SET
命令为例):
lua
-- KEYS[1] = 锁的Key(如 "my_lock")
-- ARGV[1] = 锁的过期时间(毫秒)
-- ARGV[2] = 客户端唯一标识(UUID:threadId)
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); -- 重入锁,计数器+1
redis.call('pexpire', KEYS[1], ARGV[1]); -- 续期
return nil;
end;
return redis.call('pttl', KEYS[1]); -- 返回锁的剩余存活时间(加锁失败)
- Hash 结构存储:锁的 Key 对应一个 Hash,字段为客户端标识,值为重入次数(支持可重入锁)。
二、解锁时的严格验证
1. 解锁 Lua 脚本
Redisson 解锁时通过原子性 Lua 脚本验证线程标识:
lua
-- KEYS[1] = 锁的Key(如 "my_lock")
-- KEYS[2] = 解锁消息的Channel(如 "redisson_unlock")
-- ARGV[1] = 锁的过期时间(毫秒)
-- ARGV[2] = 客户端唯一标识(UUID:threadId)
-- 检查锁是否存在且标识匹配
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then
return nil; -- 标识不匹配,直接返回(无法解锁)
end;
-- 减少重入次数(如果是可重入锁)
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[1]); -- 重入次数未归零,仅续期
return 0;
else
redis.call('del', KEYS[1]); -- 重入次数归零,删除锁
redis.call('publish', KEYS[2], ARGV[1]); -- 发布解锁消息(通知其他等待线程)
return 1;
end;
return nil;
2. 关键保障机制
- 标识匹配 :只有 Hash 中的字段(
ARGV[2]
)与当前客户端标识一致时才会解锁。 - 原子性操作:Lua 脚本保证验证和删除是原子操作,避免并发问题。
- 可重入支持:通过计数器实现同一线程多次加锁/解锁。
三、异常情况处理
1. 线程崩溃或网络断开
- 锁自动释放:Redisson 默认设置锁的过期时间(避免死锁),即使线程崩溃,锁也会因超时被 Redis 自动删除。
- WatchDog 机制:如果未显式设置过期时间,Redisson 会启动后台线程(WatchDog)定期续期,确保业务执行期间锁不会意外释放。
2. 锁被误删的防御
- 场景:其他线程或客户端尝试解锁。
- 防御:Lua 脚本严格校验标识,非所有者解锁操作会被拒绝。
四、完整流程示例
1. 加锁
java
RLock lock = redisson.getLock("my_lock");
lock.lock(); // 内部生成UUID:threadId并写入Redis
2. 解锁
java
lock.unlock(); // 通过Lua脚本验证标识并释放锁
3. Redis 中的数据状态
bash
# 加锁后的Redis数据
HGETALL my_lock
1) "4a9d6c80-1a2b-3c4d-5e6f-7a8b9c0d1e2f:1" # 字段:客户端标识
2) "1" # 值:重入次数
五、与其他方案的对比
方案 | 实现方式 | 线程安全 | 缺点 |
---|---|---|---|
Redisson | UUID + 线程ID + Lua 验证 | 高 | 依赖Redis |
纯SETNX + 随机值 | 客户端生成随机值作为标识 | 中 | 需自行实现续期和重入逻辑 |
ZK临时节点 | 基于ZK的临时顺序节点 | 高 | 性能较低,依赖ZK集群 |
六、生产环境注意事项
- 避免锁过期时间过短:确保大于业务执行时间,或启用 WatchDog。
- 禁止跨线程解锁:不同线程的标识不同,会导致解锁失败。
- 监控锁竞争 :通过
redisson.getLock("my_lock").isLocked()
检查锁状态。
总结
Redisson 通过 客户端唯一标识 和 原子性 Lua 脚本 的双重保障,严格限制了只有加锁的线程才能解锁。这种设计在分布式环境下既保证了安全性,又通过可重入机制和 WatchDog 优化了用户体验,是分布式锁的高可靠性实现方案。