Redisson 看门狗机制详解:分布式锁如何自动续期防止提前过期

Redisson 看门狗机制详解:分布式锁如何自动续期防止提前过期

一、看门狗要解决什么问题

1.1 锁超时的两难困境

给分布式锁设置超时时间是为了防止死锁(持有者崩溃后锁能自动释放)。但超时时间设多少是个难题:

设置 风险
设太短(如5秒) 业务还没执行完,锁就过期了,其他线程趁虚而入
设太长(如10分钟) 持有者崩溃后,其他线程要等10分钟才能获取锁

举个例子

你设置锁超时30秒,正常情况下业务2秒就完成了。但某天数据库慢查询导致业务执行了35秒:

复制代码
线程A                              线程B
  │                                  │
  ├── 加锁(超时30秒)                │
  ├── 开始执行业务...                  │
  │   (数据库慢查询中...)             │
  │                                  │
  ├── 第30秒:锁自动过期!             │
  │                                  ├── 加锁成功(锁已过期)
  │   (还在执行中...)                ├── 开始操作同一数据
  │                                  │
  ├── 第35秒:业务完成                 │
  ├── 释放锁 → 释放的是线程B的锁!     │
  │                                  │
  结果:两个线程同时操作了同一数据,数据不一致

1.2 看门狗的解决思路

不设固定的超时时间,而是让一个后台线程(看门狗)定期检查锁是否还在使用,如果还在使用就自动延长过期时间。

就像你在图书馆占了一个座位,规定2小时不用就收回。看门狗就是你的朋友,每隔1小时帮你去前台说"他还在用",这样座位就不会被收回。直到你真的离开了,朋友也不再帮你续期,座位自然就释放了。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、看门狗的工作原理

2.1 核心流程

复制代码
加锁成功(设置初始过期时间30秒)
    │
    ├── 启动看门狗定时任务(每10秒执行一次)
    │
    │   ┌─── 第10秒:检查锁是否还被持有 → 是 → 续期到30秒
    │   │
    │   ├─── 第20秒:检查锁是否还被持有 → 是 → 续期到30秒
    │   │
    │   ├─── 第30秒:检查锁是否还被持有 → 是 → 续期到30秒
    │   │
    │   ├─── 第40秒:检查锁是否还被持有 → 是 → 续期到30秒
    │   │
    │   └─── ...(持续续期,直到主动释放锁)
    │
    ▼
主动释放锁 → 停止看门狗 → 锁正常过期

2.2 关键参数

参数 Redisson 默认值 含义
lockWatchdogTimeout 30秒 锁的初始过期时间,也是每次续期的时长
续期间隔 过期时间的 1/3(即10秒) 每隔多久检查一次是否需要续期

2.3 看门狗何时停止

场景 看门狗行为
主动调用 unlock() 立即停止续期,删除锁
持有锁的线程执行完毕 停止续期,锁到期后自动释放
持有锁的 JVM 进程崩溃 看门狗随进程一起消失,锁到期后自动释放
网络断开(与 Redis 断连) 续期失败,锁到期后自动释放

三、Redisson 中的使用方式

3.1 启用看门狗(不指定 leaseTime)

java 复制代码
RLock lock = redissonClient.getLock("my-lock");

// 只指定等待时间,不指定 leaseTime → 自动启用看门狗
lock.tryLock(10, TimeUnit.SECONDS);

try {
    // 业务逻辑,无论执行多久,锁都不会过期
    doSlowBusinessLogic();
} finally {
    lock.unlock(); // 主动释放,看门狗停止
}

3.2 禁用看门狗(指定 leaseTime)

java 复制代码
RLock lock = redissonClient.getLock("my-lock");

// 指定 leaseTime=30秒 → 不启用看门狗,30秒后强制过期
lock.tryLock(10, 30, TimeUnit.SECONDS);

try {
    // 如果业务超过30秒,锁会自动释放!
    doBusinessLogic();
} finally {
    lock.unlock();
}

3.3 对比

方式 代码 看门狗 适用场景
自动续期 tryLock(waitTime) 启用 业务耗时不确定
固定超时 tryLock(waitTime, leaseTime) 不启用 业务耗时可预估

四、看门狗的内部实现(简化版)

4.1 伪代码实现

java 复制代码
/**
 * 看门狗机制的简化实现.
 */
public class WatchdogLock {

    private final RedisTemplate<String, String> redisTemplate;
    private final String lockKey;
    private final String holderId;
    private final long watchdogTimeout; // 默认30秒
    private ScheduledFuture<?> renewalTask; // 续期定时任务
    private final ScheduledExecutorService scheduler;

    /**
     * 加锁并启动看门狗.
     */
    public boolean tryLock(long waitTime, TimeUnit unit) {
        // 尝试加锁,设置初始过期时间
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, holderId,
                watchdogTimeout, TimeUnit.MILLISECONDS);

        if (Boolean.TRUE.equals(success)) {
            // 加锁成功,启动看门狗
            startWatchdog();
            return true;
        }
        return false;
    }

    /**
     * 启动看门狗定时任务.
     * 每隔 watchdogTimeout/3 的时间检查并续期.
     */
    private void startWatchdog() {
        long renewInterval = watchdogTimeout / 3; // 10秒

        renewalTask = scheduler.scheduleAtFixedRate(() -> {
            // 检查锁是否还是自己持有的
            String currentHolder =
                redisTemplate.opsForValue().get(lockKey);
            if (holderId.equals(currentHolder)) {
                // 续期:重新设置过期时间
                redisTemplate.expire(lockKey,
                    watchdogTimeout, TimeUnit.MILLISECONDS);
            } else {
                // 锁已经不是自己的了,停止续期
                stopWatchdog();
            }
        }, renewInterval, renewInterval, TimeUnit.MILLISECONDS);
    }

    /**
     * 释放锁并停止看门狗.
     */
    public void unlock() {
        // 停止看门狗
        stopWatchdog();

        // 释放锁(验证持有者)
        String currentHolder =
            redisTemplate.opsForValue().get(lockKey);
        if (holderId.equals(currentHolder)) {
            redisTemplate.delete(lockKey);
        }
    }

    /**
     * 停止看门狗定时任务.
     */
    private void stopWatchdog() {
        if (renewalTask != null && !renewalTask.isCancelled()) {
            renewalTask.cancel(false);
        }
    }
}

4.2 续期的 Redis 命令

看门狗每次续期实际执行的是:

复制代码
# 检查锁是否还是自己持有的,如果是则续期
# 使用 Lua 脚本保证原子性
EVAL "
  if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('pexpire', KEYS[1], ARGV[2])
  else
    return 0
  end
" 1 "my-lock" "holder-id-123" "30000"

用 Lua 脚本是为了保证"检查持有者"和"续期"这两步是原子操作,不会被其他命令打断。


五、看门狗的时序图

5.1 正常场景:业务执行时间超过锁的初始过期时间

复制代码
时间轴(秒)  0    10    20    30    35    40
              │     │     │     │     │     │
加锁(30s)     ├─────────────────────────────┤ 没有看门狗的话这里锁就过期了
              │     │     │     │     │     │
看门狗续期    │     ├→续期  │     │     │
              │     │     ├→续期  │     │
              │     │     │     ├→续期  │
              │     │     │     │     │     │
业务执行      ├─────────────────────────┤    │
              │     │     │     │     │     │
unlock()      │     │     │     │     ├→释放锁,停止看门狗
              │     │     │     │     │     │

结果:业务执行了35秒,但锁始终有效,没有被其他线程抢走

5.2 异常场景:JVM 崩溃

复制代码
时间轴(秒)  0    10    15    20    30    40
              │     │     │     │     │     │
加锁(30s)     ├─────────────────────────────┤
              │     │     │     │     │     │
看门狗续期    │     ├→续期  │     │     │
              │     │     │     │     │     │
JVM崩溃       │     │     ✗     │     │     │  ← 第15秒进程崩溃
              │     │           │     │     │
              │     │           │     │     │  看门狗随进程消失
              │     │           │     │     │  最后一次续期在第10秒
              │     │           │     │     │  锁将在第40秒过期(10+30)
              │     │           │     │     │
其他线程      │     │           │     │     ├→ 第40秒:锁过期,可以获取

结果:进程崩溃后最多等30秒(一个续期周期),锁自动释放

六、什么时候用看门狗,什么时候不用

6.1 推荐使用看门狗的场景

场景 原因
业务耗时不确定(如调用外部接口) 无法预估超时时间
涉及大量数据库操作 慢查询可能导致耗时波动
后台异步任务 处理时间可能很长

6.2 不推荐使用看门狗的场景

场景 原因 建议
秒杀/抢购 需要快速失败,不应长时间持有锁 固定短超时
简单的状态更新 耗时可预估(毫秒级) 固定超时即可
对锁释放时间有严格要求 看门狗续期可能导致锁持有过久 固定超时

6.3 代码选择

java 复制代码
// 场景一:后台订单处理(耗时不确定)→ 用看门狗
lock.tryLock(30, TimeUnit.SECONDS); // 不指定 leaseTime

// 场景二:秒杀扣库存(必须快速完成)→ 不用看门狗
lock.tryLock(5, 10, TimeUnit.SECONDS); // 指定 leaseTime=10秒

七、看门狗的注意事项

7.1 必须主动调用 unlock()

看门狗会一直续期,如果你忘记调用 unlock(),锁会被无限续期,永远不释放(直到 JVM 退出)。

java 复制代码
// 错误:忘记 unlock,锁永远不释放
public void badMethod() {
    RLock lock = redissonClient.getLock("key");
    lock.lock(); // 看门狗启动

    doSomething(); // 如果这里抛异常...
    // unlock 永远不会被调用!看门狗持续续期!
}

// 正确:用 try-finally 保证释放
public void goodMethod() {
    RLock lock = redissonClient.getLock("key");
    lock.lock();
    try {
        doSomething();
    } finally {
        lock.unlock(); // 无论是否异常都释放
    }
}

7.2 看门狗是线程级别的

看门狗绑定的是"加锁的线程"。如果加锁线程结束了但没有调用 unlock,看门狗的行为取决于实现:

  • Redisson:看门狗任务在 Netty 的事件循环中运行,即使加锁线程结束,只要 JVM 还在,看门狗仍会续期
  • 所以一定要确保 unlock 被调用

7.3 集群环境下的可靠性

看门狗续期依赖与 Redis 的网络连接。如果网络断开:

复制代码
正常:看门狗 → Redis(续期成功)
断网:看门狗 → X → Redis(续期失败)
    → 锁在上次续期后的30秒内过期
    → 其他实例可以获取锁

这是一种"安全失败"的设计:宁可让锁过期(可能短暂重复处理),也不让锁永远不释放(死锁)。


八、完整示例:带看门狗的文件导入任务

java 复制代码
/**
 * 文件导入服务.
 * 导入大文件可能耗时几分钟,使用看门狗防止锁提前过期.
 */
@Service
public class FileImportServiceImpl implements FileImportService {

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private FileRecordRepository fileRecordRepository;

    /**
     * 导入文件(耗时不确定,可能几秒也可能几分钟).
     */
    public void importFile(Integer fileId) {
        // 按文件ID加锁,防止同一文件被重复导入
        String lockKey = "file-import-" + fileId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 只指定等待时间,不指定 leaseTime → 启用看门狗
            boolean acquired = lock.tryLock(10, TimeUnit.SECONDS);
            if (!acquired) {
                log.warn("文件正在被其他实例导入, fileId:{}", fileId);
                return;
            }

            // 看门狗已启动,无论导入多久锁都不会过期
            log.info("开始导入文件, fileId:{}", fileId);

            // 模拟耗时操作:逐行读取并处理
            FileRecord fileRecord = fileRecordRepository
                .findById(fileId).orElseThrow();
            List<String> lines = readFileLines(fileRecord.getFilePath());

            for (int i = 0; i < lines.size(); i++) {
                processLine(lines.get(i));

                // 每处理1000行打印进度
                if (i % 1000 == 0) {
                    log.info("导入进度: {}/{}, fileId:{}",
                        i, lines.size(), fileId);
                }
            }

            // 更新状态
            fileRecord.setStatus("COMPLETED");
            fileRecordRepository.save(fileRecord);
            log.info("文件导入完成, fileId:{}, 总行数:{}",
                fileId, lines.size());

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.warn("文件导入被中断, fileId:{}", fileId);
        } catch (Exception e) {
            log.error("文件导入异常, fileId:{}", fileId, e);
        } finally {
            // 释放锁,看门狗停止
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

执行过程

复制代码
第0秒:  加锁成功,看门狗启动(每10秒续期一次)
第10秒: 看门狗续期 → 锁过期时间重置为30秒后
第20秒: 看门狗续期 → 锁过期时间重置为30秒后
...
第180秒:文件导入完成(处理了3分钟)
第180秒:调用 unlock() → 锁释放,看门狗停止

整个过程中锁始终有效,不会被其他实例抢走

九、总结

问题 答案
看门狗是什么? 一个后台定时任务,定期给锁续期
为什么需要看门狗? 解决"锁超时时间难以确定"的问题
什么时候启用? 调用 tryLock 时不指定 leaseTime
续期频率是多少? 默认每 10 秒一次(过期时间的 1/3)
进程崩溃后锁会一直存在吗? 不会,看门狗随进程消失,锁在最后一次续期后30秒过期
忘记 unlock 会怎样? 锁被无限续期,永远不释放(直到 JVM 退出)
和固定超时的区别? 看门狗适合耗时不确定的场景,固定超时适合耗时可预估的场景
相关推荐
霸道流氓气质16 小时前
Redisson 分布式集合详解:像用本地集合一样操作跨服务共享数据
分布式
phltxy16 小时前
RabbitMQ高级特性-消息确认与持久性博客
分布式·rabbitmq·ruby
2603_9547083117 小时前
协调控制柜在微电网中的核心地位:数据枢纽、控制核心、安全屏障
分布式·安全·架构·能源·需求分析
淡漠的蓝精灵17 小时前
Pulsar 入门:云原生分布式消息流平台
分布式·其他·云原生
ai生成式引擎优化技术19 小时前
DLOS Kernel v1.0:面向分布式AI任务执行与Agent调度的统一运行时内核
人工智能·分布式
ai生成式引擎优化技术19 小时前
DLOS v0.7:面向分布式多智能体AI操作系统的自进化内核
人工智能·分布式
未若君雅裁19 小时前
RabbitMQ 消息可靠性:生产者确认、持久化、消费者ACK与幂等消费
分布式·微服务·rabbitmq
数据库小学妹19 小时前
分布式数据库架构演进:从集中式到分布式,三大路线一次讲清楚
数据库·分布式·数据库架构
juniperhan19 小时前
Flink 系列第25篇:Flink SQL 集成 Hive 实践:流批一体下的实时数仓利器
大数据·数据仓库·hive·分布式·sql·flink