一、核心问题与解决方案
问题本质
ClientA Redis ClientB SET lock_key clientA_id EX 30 业务处理中... 锁超时自动释放 SET lock_key clientB_id EX 30 DEL lock_key 锁被意外释放! ClientA Redis ClientB
Redisson解决方案
- 客户端唯一ID:UUID+线程ID作为锁标识
- Lua脚本原子操作:校验+删除一步完成
- 看门狗机制:后台线程定期续期
- 发布订阅:锁释放通知避免无效轮询
二、源码实现解析
1. 加锁流程(Lock操作)
核心Lua脚本
lua
-- KEYS[1]: 锁key
-- ARGV[1]: 锁过期时间(毫秒)
-- ARGV[2]: 客户端唯一标识(UUID:threadId)
-- 锁不存在时加锁
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1) -- 使用hash存储
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])
看门狗启动逻辑(RedissonLock类)
java
private void scheduleExpirationRenewal(long threadId) {
// 创建定时任务
Timeout task = commandExecutor.getConnectionManager()
.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
// 检查锁是否仍被当前线程持有
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
// 异常处理
return;
}
if (res) {
// 递归调用,实现周期性续期
scheduleExpirationRenewal(threadId);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 默认30s/3=10s执行一次
}
续期Lua脚本
lua
-- KEYS[1]: 锁key
-- ARGV[1]: 续期时间(默认30s)
-- ARGV[2]: 客户端唯一标识
-- 检查是否仍是当前线程持有锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 续期
redis.call('pexpire', KEYS[1], ARGV[1])
return 1
end
return 0
2. 解锁流程(Unlock操作)
解锁Lua脚本
lua
-- KEYS[1]: 锁key
-- KEYS[2]: 发布订阅频道
-- ARGV[1]: 释放锁消息(0L)
-- ARGV[2]: 锁过期时间
-- ARGV[3]: 客户端唯一标识
-- 锁不存在(可能已过期)
if (redis.call('exists', KEYS[1]) == 0) then
-- 发布解锁消息
redis.call('publish', KEYS[2], ARGV[1])
return 1
end
-- 非当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil
end
-- 减少重入计数
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1)
if (counter > 0) then
-- 重入次数>0,仅更新过期时间
redis.call('pexpire', KEYS[1], ARGV[2])
return 0
else
-- 完全释放锁
redis.call('del', KEYS[1])
-- 发布解锁消息
redis.call('publish', KEYS[2], ARGV[1])
return 1
end
锁释放通知机制
java
// RedissonLock.unlockAsync方法
protected RFuture<Boolean> unlockAsync(long threadId) {
// 执行解锁Lua脚本
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.onComplete((res, e) -> {
// 取消看门狗任务
cancelExpirationRenewal(threadId);
if (e != null) {
// 异常处理
return;
}
if (res == null) {
// 锁非当前线程持有
throw new IllegalMonitorStateException();
}
});
return future;
}
3. 锁等待机制
请求线程(Thread B) Redis服务器 发布订阅频道 锁持有线程(Thread A) 初始状态:Thread A持有锁 尝试获取锁 (lua脚本) 返回锁剩余TTL(500ms) 订阅锁释放频道 (SUBSCRIBE) 等待指定时间(500ms) 收到锁释放消息 重新尝试获取锁 直接尝试获取锁 alt [等待期间收到通知] [等待超时未收到通知] loop [等待锁释放] 取消订阅 (UNSUBSCRIBE) 执行业务逻辑 取消订阅 (UNSUBSCRIBE) 返回获取锁失败 alt [获取锁成功] [获取锁失败] Thread A释放锁 执行解锁lua脚本 发布锁释放消息(PUBLISH) 请求线程(Thread B) Redis服务器 发布订阅频道 锁持有线程(Thread A)
三、完整工作流程
加锁流程
是 否 尝试加锁 成功? 启动看门狗线程 获取锁剩余TTL 订阅锁释放频道 等待TTL时间 执行业务逻辑
解锁流程
是 是 否 否 执行解锁Lua脚本 校验通过? 减少重入计数 计数=0? 删除锁并发布通知 更新过期时间 抛出异常 取消看门狗任务
四、关键设计亮点
-
可重入锁设计:
- 使用Hash结构存储
clientId:重入次数
- 避免同一线程多次加锁导致死锁
- 使用Hash结构存储
-
锁续命机制:
- 默认每10秒续期一次(internalLockLeaseTime/3)
- 续期时间可配置(默认30秒)
-
高效等待机制:
java// 订阅锁释放通知 RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId); if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) { // 超时处理 }
-
容错处理:
- 网络异常时自动重试
- 加锁超时自动取消
- 看门狗线程异常自动终止
五、最佳实践建议
-
锁命名规范:
java// 使用业务前缀+资源ID RLock lock = redisson.getLock("order:pay:" + orderId);
-
超时时间设置:
java// 根据业务最大耗时设置 lock.lock(10, TimeUnit.SECONDS);
-
避免长事务:
- 超过30秒的业务考虑拆分
- 监控看门狗日志,警惕续期失败
-
集群环境特别配置:
javaConfig config = new Config(); config.useClusterServers() .setCheckSlotsCoverage(false); // 避免slot覆盖检查
六、性能优化点
-
减少网络往返:
- 所有关键操作用Lua脚本实现
- 单次请求完成多个操作
-
避免无效轮询:
- 通过发布订阅通知等待线程
- 精确控制重试时机
-
轻量级看门狗:
- 定时任务而非持续轮询
- 空闲时自动释放资源
通过Redisson的这套实现,分布式锁在保证安全性的同时,实现了高性能和高可用性,是生产环境的首选方案。