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 优化了用户体验,是分布式锁的高可靠性实现方案。

相关推荐
CopyLower41 分钟前
分布式ID生成方案的深度解析与Java实现
java·开发语言·分布式
whoarethenext3 小时前
qt的基本使用
开发语言·c++·后端·qt
m0_684598534 小时前
如何开发英语在线训练小程序:从0到1的详细步骤
java·微信小程序·小程序·小程序开发
ml130185288744 小时前
开发一个环保回收小程序需要哪些功能?环保回收小程序
java·大数据·微信小程序·小程序·开源软件
zybishe5 小时前
免费送源码:Java+ssm+MySQL 酒店预订管理系统的设计与实现 计算机毕业设计原创定制
java·大数据·python·mysql·微信小程序·php·课程设计
anlogic6 小时前
Java基础 4.12
java·开发语言
weisian1516 小时前
Java常用工具算法-7--秘钥托管云服务2(阿里云 KMS)
java·安全·阿里云
草捏子6 小时前
主从延迟导致数据读不到?手把手教你架构级解决方案
后端
拉不动的猪6 小时前
设计模式之------单例模式
前端·javascript·面试
橘猫云计算机设计7 小时前
基于Python电影数据的实时分析可视化系统(源码+lw+部署文档+讲解),源码可白嫖!
数据库·后端·python·信息可视化·小程序·毕业设计