Redisson 分布式锁
·
目录
三、看门狗(Watchdog)机制:解决 “业务没跑完,锁先过期”
1. 启动看门狗:scheduleExpirationRenewal
3. 真正发 Lua 续期:renewExpirationAsync
4. 解锁时停止看门狗:cancelExpirationRenewal
一、Redisson 分布式锁整体介绍
1. 核心是什么
Redisson 的分布式锁 RLock 是基于 Redis + Lua 原子脚本实现的可重入、阻塞、带自动续期的分布式锁,几乎对标 Java 的 ReentrantLock,但跑在分布式环境。
2. 核心特性
- 互斥性:同一时刻只有一个客户端线程能拿到锁。
- 可重入:同一线程可以多次
lock(),计数递增,不会自己死锁。 - 阻塞 / 非阻塞:支持
lock()(阻塞)、tryLock()(非阻塞 / 超时)。 - 自动过期:锁有 TTL,防止客户端宕机死锁。
- 看门狗续期:业务没跑完就自动延长锁时间,避免误释放。
- 释放唤醒:解锁时用 Redis Pub/Sub 通知等待线程,避免空轮询。
二、加锁与解锁核心流程(Lua 原子脚本)
1. 加锁脚本(简化)
-- KEYS[1]=lockKey, ARGV[1]=uuid:threadId, ARGV[2]=ttl
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[1], 1)
redis.call('pexpire', KEYS[1], ARGV[2])
return nil
end
-- 锁是自己的,重入
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
redis.call('hincrby', KEYS[1], ARGV[1], 1)
redis.call('pexpire', KEYS[1], ARGV[2])
return nil
end
-- 锁被别人持有,返回剩余 TTL
return redis.call('pttl', KEYS[1])
- 不存在 → 加锁 + 设置 TTL
- 存在且是自己 → 重入计数 + 1 + 刷新 TTL
- 存在且不是自己 → 返回剩余时间 → 抢锁失败
2. 解锁脚本(简化)
-- KEYS[1]=lockKey, KEYS[2]=channel, ARGV[1]=uuid:threadId, ARGV[2]=ttl
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
return nil -- 不是自己的锁,不能解
end
local cnt = redis.call('hincrby', KEYS[1], ARGV[1], -1)
if cnt > 0 then
redis.call('pexpire', KEYS[1], ARGV[2])
return 0
else
redis.call('del', KEYS[1])
redis.call('publish', KEYS[2], 'release') -- 发通知
return 1
end
- 必须是自己的锁才能解
- 重入计数减 1
- 计数 >0:只刷新 TTL,不删 key
- 计数 =0:删 key + 发解锁通知
三、看门狗(Watchdog)机制:解决 “业务没跑完,锁先过期”
1. 看门狗作用
- 当你调用
lock()(不指定 leaseTime)时,Redisson 启动看门狗。 - 默认:
lockWatchdogTimeout = 30000 ms(锁初始 TTL)- 每 10s(1/3) 自动续期一次,把锁 TTL 重置为 30s
- 一旦
unlock()或线程宕机 → 看门狗停止 → 锁自然过期。
2. 什么时候启用 / 禁用
- ✅ 启用看门狗:
lock.lock(); // 无参 lock.lockInterruptibly(); lock.tryLock(-1, TimeUnit.SECONDS); - ❌ 禁用看门狗(指定了 leaseTime):
lock.lock(10, TimeUnit.SECONDS); // 10s 后必释放 lock.tryLock(5, TimeUnit.SECONDS);
四、看门狗源码逐行解析(Redisson 3.x)
入口:lock() → tryAcquireAsync() → scheduleExpirationRenewal()
1. 启动看门狗:scheduleExpirationRenewal
// RedissonBaseLock
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
// map: lockName -> ExpirationEntry(管理该锁的所有线程)
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
renewExpiration(); // 第一次:启动续期任务
}
}
EXPIRATION_RENEWAL_MAP:全局 static map,记录 “哪个锁 + 哪些线程” 需要续期。- 新锁 → 调用
renewExpiration()开始循环续期。
2. 核心续期逻辑:renewExpiration
// RedissonBaseLock
protected void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return; // 锁已释放,不用续了
}
// 延时任务:10s 后执行
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 异步续期:执行 Lua 脚本,把 TTL 重置为 30s
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock {} expiration", getRawName(), e);
return;
}
if (res) {
renewExpiration(); // 续期成功 → 递归:再等 10s 再续
}
// res=false:锁不存在了(已释放)→ 不递归,任务终止
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 10s
}
关键点:
newTimeout(..., 10s):10 秒后执行一次续期。renewExpirationAsync(threadId):发 Lua 脚本到 Redis,续期 TTL。- 递归调用 renewExpiration ():只要锁还在,就每 10s 续一次,形成 “无限循环” 直到解锁。
3. 真正发 Lua 续期:renewExpirationAsync
private RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// Lua 脚本:只有锁存在且是自己的,才续期
"if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return true; " +
"end; " +
"return false;",
Collections.singletonList(getRawName()),
getLockName(threadId), internalLockLeaseTime); // 30s
}
逻辑:
- 检查
hash 里有自己的 threadId 吗? - 有 →
pexpire重置为 30s → 返回 true → 继续续期 - 没有 → 返回 false → 看门狗停止
4. 解锁时停止看门狗:cancelExpirationRenewal
protected void cancelExpirationRenewal(Long threadId) {
ExpirationEntry entry = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (entry == null) {
return;
}
if (threadId != null) {
entry.removeThreadId(threadId);
}
// 没有线程再持有这把锁 → 从 map 移除 → 下次 renewExpiration 直接 return
if (entry.isEmpty()) {
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
unlock()时调用- 把当前线程从
ExpirationEntry移除 - 空了就删 key → 看门狗下次执行发现
ee == null→ 终止循环
五、整体时序(带看门狗)
lock()→ Redis:不存在 → set + expire 30s- 启动看门狗:10s 后执行
- 10s 到 → Lua:锁存在且是自己 → expire 30s → 再等 10s
- 业务跑完 →
unlock()→ 删 key + 停看门狗 - 如果宕机 → 看门狗线程死 → 不再续期 → 30s 后 Redis 自动删 key
六、常见面试点总结
- 看门狗什么时候开? 无参
lock(),不指定leaseTime。 - 默认参数? 30s TTL,每 10s 续期。
- 续期原理? 定时任务 + Lua 原子检查 + 递归循环。
- 怎么停?
unlock()主动停;宕机被动停。 - 和普通 Redis 锁区别? 自动续期、可重入、Pub/Sub 唤醒、防死锁更完善。
更多推荐



所有评论(0)