一、Watchdog 自动续期机制
问题:
如果设置锁过期时间,例如 30s,但业务执行 40s,锁会提前释放,导致其他线程进入临界区。
Watchdog 的思路:
- 加锁时 不设置固定过期时间
- 启动一个 后台定时任务
- 每隔一段时间 刷新锁的 TTL
- 只要持锁线程还活着就不断续期
- 线程释放锁后停止续期
Redisson 默认:
- 锁时间:30s
- 续期周期:10s(1/3)
流程:
线程A获取锁 ↓ Redis key TTL = 30s ↓ 后台 watchdog 每10秒执行 ↓ PEXPIRE key 30000 ↓ 不断续期 ↓ 线程A释放锁 ↓ 停止续期
二、Watchdog 核心代码示例
简化版 Java 实现逻辑(模拟 Redisson):
java
public class RedisDistributedLock {
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private StringRedisTemplate redisTemplate;
private String lockKey;
private String lockValue;
private int expireTime = 30;
public boolean lock() {
lockValue = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
startWatchDog();
return true;
}
return false;
}
private void startWatchDog() {
scheduler.scheduleAtFixedRate(() -> {
String value = redisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(value)) {
redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
}
}, 10, 10, TimeUnit.SECONDS);
}
public void unlock() {
String value = redisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(value)) {
redisTemplate.delete(lockKey);
}
scheduler.shutdown();
}
}
核心逻辑:
- setnx + expire 获取锁
- 后台线程定期执行 expire
- 只给自己加的锁续期
- 释放锁时停止 watchdog
三、Redisson 的真实 Watchdog 机制
Redisson 的实现更复杂:
核心逻辑:
lock() ↓ 设置锁 TTL = 30s ↓ 启动 watchdog ↓ scheduleExpirationRenewal() ↓ 每 10 秒执行 ↓ renewExpirationAsync() ↓ PEXPIRE key 30000
源码关键代码(简化):
java
private void scheduleExpirationRenewal(long threadId) {
Timeout task = timer.newTimeout(timeout -> {
renewExpirationAsync(threadId);
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}
续期脚本:
Lua
if redis.call("hexists", KEYS[1], ARGV[2]) == 1 then
redis.call("pexpire", KEYS[1], ARGV[1])
return 1
end
return 0
意思:
- 如果 当前线程仍然持有锁
- 执行
PEXPIRE - 继续续期
四、Redis 主从延迟导致的问题
Redis 主从复制是 异步复制。
可能发生:
客户端A ↓ master SET lock ↓ 返回成功 (还没同步) master 宕机 ↓ slave 升级为 master 客户端B ↓ 在新 master 上 SET lock ↓ 成功
最终:
客户端A 持锁 客户端B 也持锁
两个客户端同时进入临界区
这就是 Redis 分布式锁最大的问题之一。
五、RedLock 解决方案
RedLock 需要:
5个独立 Redis 节点
加锁流程:
客户端依次向 5 个 Redis 加锁 成功 >= 3 才算成功
例如:
R1 成功 R2 成功 R3 成功 R4 失败 R5 失败
3/5 成功 → 加锁成功
如果:
R1 成功 R2 成功 R3 失败 R4 失败 R5 失败
2/5 → 加锁失败
并且要满足:
获取锁总耗时 < 锁过期时间
六、Redisson 的 RedLock 示例
java
RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
// 业务代码
}
} finally {
redLock.unlock();
}
七、实际生产建议
业内常见实践:
1️⃣ 单 Redis + Lua 脚本 + Watchdog
适用于:
- 大多数互联网系统
- 高性能
2️⃣ Redis Cluster + Redisson
优点:
- 自动续期
- Lua 保证原子性
- API 简单
3️⃣ 对一致性要求极高
使用:
- ZooKeeper
- etcd
因为:
Redis 是 AP ZooKeeper / etcd 是 CP
八、一个很多人不知道的面试点
Redis 分布式锁 还有一个隐藏问题:
客户端 GC pause
例如:
线程A获取锁 TTL=30s 发生 Full GC 40s 锁已过期 线程B获得锁 GC结束 线程A继续执行
又出现并发
解决方案:
Fencing Token(围栏令牌)
很多分布式系统(如 Kafka / ZooKeeper)都会用这个机制。
====================================================
Redis 本身没有提供"自动续期锁"的官方机制 。
Redis 只提供基础命令,比如:
SET key value NX PX 30000EXPIRE keyPEXPIRE key
自动续期(Watchdog)必须由客户端自己实现,或者使用第三方库实现。Redis 服务器不会自动帮你续期。
一、Redis原生命令能做什么
Redis只提供修改 TTL 的命令,例如:
设置过期时间:
SET lock_key uuid NX PX 30000
续期:
PEXPIRE lock_key 30000
或者
EXPIRE lock_key 30
但 Redis 不会自动调用这些命令,需要客户端定时执行。
二、最简单的手动实现方式
应用程序自己开 定时任务。
示例:
java
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
String value = redisTemplate.opsForValue().get("lock_key");
if ("uuid123".equals(value)) {
redisTemplate.expire("lock_key", 30, TimeUnit.SECONDS);
}
}, 10, 10, TimeUnit.SECONDS);
逻辑:
加锁成功 ↓ 启动定时任务 ↓ 每10秒执行 ↓ 检查锁是不是自己的 ↓ 是 → expire 续期
三、为什么要判断是不是自己的锁
如果不判断会出现问题:
线程A加锁 TTL=30s 线程A卡住 锁过期 线程B获得锁 线程A的定时任务继续续期
结果:
A把B的锁续期了
所以必须校验:
value == uuid
四、更安全的续期方式(Lua脚本)
实际生产中会用 Lua 保证原子性:
java
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end
Java 调用:
java
redisTemplate.execute(luaScript,
Collections.singletonList("lock_key"),
uuid, "30000");
保证:
检查锁 + 续期
是 原子操作。
五、为什么大家直接用 Redisson
因为自己实现会遇到很多坑:
- watchdog线程管理
- Lua原子操作
- 锁重入
- 锁自动续期
- 线程ID识别
- GC暂停问题
- 锁释放安全
Redisson 已经帮你全部实现好了。
示例:
java
RLock lock = redissonClient.getLock("order_lock");
lock.lock();
默认行为:
TTL = 30s watchdog 每 10s 自动续期
释放:
lock.unlock();
六、总结
Redis官方能力:
SET NX PX EXPIRE PEXPIRE Lua
但 没有自动续期机制。
实现方式:
1️⃣ 自己写定时任务 + EXPIRE
2️⃣ Lua脚本保证原子续期
3️⃣ 使用 Redisson(最常见)
