Redisson如何保证解锁的线程一定是加锁的线程?

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方请评论,我们一起交流!


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. 关键保障机制

  1. 标识匹配 :只有 Hash 中的字段(ARGV[2])与当前客户端标识一致时才会解锁。
  2. 原子性操作:Lua 脚本保证验证和删除是原子操作,避免并发问题。
  3. 可重入支持:通过计数器实现同一线程多次加锁/解锁。

三、异常情况处理

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集群

六、生产环境注意事项

  1. 避免锁过期时间过短:确保大于业务执行时间,或启用 WatchDog。
  2. 禁止跨线程解锁:不同线程的标识不同,会导致解锁失败。
  3. 监控锁竞争 :通过 redisson.getLock("my_lock").isLocked() 检查锁状态。

总结

Redisson 通过 客户端唯一标识原子性 Lua 脚本 的双重保障,严格限制了只有加锁的线程才能解锁。这种设计在分布式环境下既保证了安全性,又通过可重入机制和 WatchDog 优化了用户体验,是分布式锁的高可靠性实现方案。

相关推荐
追逐时光者17 分钟前
精选 2 款 .NET 开源、实用的缓存框架,帮助开发者更轻松地处理系统缓存!
后端·.net
David爱编程1 小时前
指令重排与内存屏障:并发语义的隐形守护者
java·后端
胡gh2 小时前
数组开会:splice说它要动刀,map说它只想看看。
javascript·后端·面试
Pure_Eyes2 小时前
go 常见面试题
开发语言·后端·golang
ZZHow10242 小时前
Java项目-苍穹外卖_Day1
java·spring boot·web
胡gh2 小时前
浏览器:我要用缓存!服务器:你缓存过期了!怎么把数据挽留住,这是个问题。
前端·面试·node.js
带刺的坐椅2 小时前
老码农教你 Solon Web Context-Path 的两种配置方式
java·nginx·tomcat·web·solon
ZZHow10243 小时前
Java项目-苍穹外卖_Day2
java·spring boot·web
Cisyam3 小时前
使用Bright Data API轻松构建LinkedIn职位数据采集系统
后端
float_六七3 小时前
Spring Boot 3为何强制要求Java 17?
java·spring boot·后端