Redis Watchdog 是分布式锁机制中一个关键的设计,用于解决锁在任务执行过程中可能因超时而释放的问题。它通过自动续期机制,确保锁在任务执行未完成时仍然有效。以下从设计原理、实现方式和使用示例等方面对其进行详细介绍。
1. Watchdog 的设计原理
在分布式锁中,为防止死锁,每个锁通常都有一个过期时间(TTL)。如果任务执行时间超过 TTL,锁可能会被自动释放,这就会导致以下问题:
- 锁被释放后,其他线程可能获取到锁,导致并发问题。
- 原任务可能被中断或错误地重复执行。
为了解决这些问题,Redis Watchdog 通过以下方式工作:
- 自动续期:在锁即将过期时,Watchdog 自动延长锁的过期时间。
- 与锁绑定:Watchdog 与锁一一绑定,仅对当前线程持有的锁生效。
- 任务完成时释放锁:当任务完成后,Watchdog 停止续期并释放锁。
2. Watchdog 的实现方式
核心机制
- 锁的唯一标识:每个锁都有一个唯一标识(UUID + 线程ID),在释放锁或续期时验证持有者身份,防止误操作。
- 定时续期任务:Watchdog 是一个后台线程,它会周期性检查锁的过期时间,并在必要时向 Redis 发送
EXPIRE命令延长 TTL。 - 过期时间动态管理:当任务完成时,Watchdog 停止续期并删除锁。
工作流程
- 获取锁时启动 Watchdog:在获取锁的同时,启动 Watchdog 定时任务。
- 定期检查锁的剩余 TTL:如果 TTL 小于一定阈值(如 2/3 的初始 TTL),则发送
EXPIRE命令续期。 - 任务完成时释放锁:停止 Watchdog 定时任务,并释放锁。
Redisson 中的实现
Redisson 是 Redis 的高级客户端,它提供了内置的 Watchdog 功能,默认的锁超时时间是 30 秒。当使用 RLock.lock() 方法时,Redisson 会自动启动一个后台线程来管理锁的续期。
以下是 Redisson 中 Watchdog 的主要实现细节:
- 锁初始 TTL:默认 30 秒(可配置)。
- 续期间隔:默认在锁的 TTL 剩余时间不足 2/3 时自动续期。
- 续期线程:Redisson 内部有一个定时任务线程池,用于定期检查锁的状态。
3. Watchdog 的实现代码示例
手动实现 Watchdog
如果不使用 Redisson,可以通过 Redis 原生命令手动实现类似的 Watchdog 机制。
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class RedisWatchdog {
private final Jedis redis;
private final String lockKey;
private final String lockValue;
private final int lockExpireTime; // 初始过期时间
private volatile boolean running = true;
public RedisWatchdog(Jedis redis, String lockKey, int lockExpireTime) {
this.redis = redis;
this.lockKey = lockKey;
this.lockExpireTime = lockExpireTime;
this.lockValue = UUID.randomUUID().toString(); // 唯一标识
}
// 加锁
public boolean lock() {
String result = redis.set(lockKey, lockValue, "NX", "PX", lockExpireTime);
if ("OK".equals(result)) {
startWatchdog();
return true;
}
return false;
}
// 解锁
public void unlock() {
if (lockValue.equals(redis.get(lockKey))) {
redis.del(lockKey);
}
stopWatchdog();
}
// 启动 Watchdog
private void startWatchdog() {
new Thread(() -> {
while (running) {
try {
Thread.sleep(lockExpireTime / 2); // 每隔一半过期时间检查
if (lockValue.equals(redis.get(lockKey))) {
redis.pexpire(lockKey, lockExpireTime); // 续期
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
// 停止 Watchdog
private void stopWatchdog() {
running = false;
}
}
使用示例
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
RedisWatchdog watchdog = new RedisWatchdog(jedis, "myLock", 10000);
if (watchdog.lock()) {
try {
// 执行任务
System.out.println("任务执行中...");
Thread.sleep(15000); // 模拟耗时任务
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
watchdog.unlock();
System.out.println("任务完成,锁已释放");
}
} else {
System.out.println("未获取到锁");
}
}
4. Watchdog 使用场景
- 长时间任务:需要保证任务执行过程中锁不会意外释放,例如批量处理任务、文件上传等。
- 高并发场景:在多个节点间共享锁资源,避免锁过期导致资源竞争或数据一致性问题。
- 分布式系统的幂等性保证:通过唯一标识和 Watchdog 续期,保证同一时刻只有一个任务在执行。
5. Redisson Watchdog 示例
Redisson 内置了 Watchdog 功能,使用起来更为简单。以下是一个 Redisson 的 Watchdog 示例:
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认锁的过期时间为 30 秒,并启动 Watchdog 自动续期
try {
// 执行耗时任务
Thread.sleep(60000); // 模拟任务耗时
} finally {
lock.unlock(); // 任务完成,释放锁,同时停止 Watchdog
}
在这个例子中,Redisson 的 Watchdog 自动管理锁的续期,你只需要调用 lock() 和 unlock() 即可。
思考题1:Redis Watchdog 不会启动场景
| 场景 | 是否启动 Watchdog | 原因 |
|---|---|---|
显示设置锁的过期时间 | 否 | 用户明确指定锁的生命周期,Redisson 不介入续期 |
| 非 Redisson 实现的分布式锁 | 否 | Watchdog 是 Redisson 的专属功能 |
| 分布式环境中未正确配置 Redisson | 否 | 错误配置导致锁管理逻辑无法正常工作 |
建议
- 如果需要自动续期功能,建议使用
lock()或未设置过期时间的tryLock(),Redisson 会默认启动 Watchdog。 - 如果手动设置锁的过期时间,但任务耗时不可预估,可以结合自动续期逻辑。
- 在复杂分布式环境中,确保 Redisson 的配置正确,以避免 Watchdog 功能失效。
思考题2: lock和trylock对比
lock()
-
作用:阻塞式加锁方法。
-
特点:
- 调用线程会一直等待,直到成功获取锁。
- 默认会启动 Watchdog(自动续期机制) ,持续延长锁的过期时间,直到显式释放锁为止。
-
使用场景
-
场景:需要确保获取锁,并且任务执行时间不可预估。
-
适用于:
- 任务耗时较长。
- 获取锁失败会导致重大问题的场景
-
tryLock()
-
作用:非阻塞或限时等待的加锁方法。
-
特点:
-
支持非阻塞模式:如果无法立即获取锁,直接返回
false。 -
支持限时等待:可以设置最大等待时间,超过该时间未获取到锁则放弃。
-
是否启动 Watchdog 取决于是否显式设置过期时间:
- 没有设置过期时间:启动 Watchdog。
- 设置了过期时间:不会启动 Watchdog。
-
-
使用场景
-
场景:尝试获取锁,不希望长时间阻塞线程。
-
适用于:
- 并发环境下需要快速放弃锁尝试的逻辑。
- 能容忍锁获取失败的场景(例如返回默认值或执行其他逻辑)。
-
| 特性/方法 | lock() | tryLock() |
|---|---|---|
| 是否阻塞 | 是,直到成功获取锁 | 可非阻塞,也可设置限时等待 |
| 等待时间 | 不支持 | 支持设置最大等待时间 |
| 锁的过期时间 | 默认无限制,需手动释放 | 可设置,超时后自动释放 |
| 自动续期(Watchdog) | 默认启动 | 条件启动(未设置过期时间时启动) |
| 适用场景 | 任务必须成功获取锁的情况 | 容忍失败,且并发较高的尝试性加锁场景 |