Java 中如何实现看门狗机制


Java 中如何实现看门狗机制?

Redisson 的看门狗(Watch Dog)机制是分布式锁的核心特性之一,其核心作用是自动为持有锁的线程续期,避免因任务执行时间过长导致锁提前释放(从而引发并发问题)。下面通过具体代码实现,并详细说明使用时的注意事项。

一、核心原理回顾

Redisson 的看门狗机制触发条件

  1. 使用tryLocklock方法时,

  2. 需要未指定锁的过期时间,即不能手动指定过期时间(或者将leaseTime = -1),

    Redisson 会自动启动后台线程(看门狗),每隔 lockWatchdogTimeout / 3 时间(默认 10 秒)为锁续期,将锁的过期时间重置为 lockWatchdogTimeout(默认 30 秒)。

直到线程主动释放锁或崩溃(此时看门狗线程终止,锁最终会过期释放)。

二、具体代码实现

1. 引入依赖(pom.xml)

<!-- Spring Boot 基础 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- Redisson 分布式锁(含看门狗机制) -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version> <!-- 兼容 Spring Boot 2.7+,3.x 需用 4.x 版本 -->
</dependency>

<!-- Redis 连接配置(Redisson 会自动依赖,可选显式声明) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 配置 Redis 和 Redisson(application.yml)

spring:
  redis:
    host: 127.0.0.1    # Redis 服务器地址
    port: 6379         # Redis 端口
    password: 123456   # Redis 密码(生产环境必须配置)
    database: 0        # 数据库索引

# 自定义看门狗参数(可选,默认值如下)
redisson:
  lockWatchdogTimeout: 60000  # 看门狗默认锁过期时间(毫秒,默认30000ms=30秒)
  singleServerConfig:
    connectTimeout: 5000      # 连接超时时间(毫秒)
    timeout: 3000             # 命令执行超时时间(毫秒)

3. Redisson 配置类(可选,自定义高级配置)

若需自定义 Redis 连接模式(如集群、哨兵),可通过配置类覆盖默认配置:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 单节点模式(生产环境推荐集群/哨兵模式,参考官方文档)
        config.useSingleServer()
               .setAddress("redis://127.0.0.1:6379")
               .setPassword("123456")
               .setDatabase(0);

        // 配置看门狗超时时间(与application.yml二选一)
        config.setLockWatchdogTimeout(60000); // 60秒

        return Redisson.create(config);
    }
}

4. 分布式锁工具类(核心实现)

封装分布式锁的获取、释放逻辑,简化业务层调用:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Component
public class RedissonDistributedLock {

    private static final Logger log = LoggerFactory.getLogger(RedissonDistributedLock.class);

    @Resource
    private RedissonClient redissonClient;

    /**
     * 获取分布式锁并执行任务(带看门狗自动续期)
     * @param lockKey 锁的唯一标识(如 "order:lock:123")
     * @param waitTime 获取锁的最大等待时间(毫秒)
     * @param task 待执行的任务
     * @return 任务执行结果
     */
    public <T> T executeWithLock(String lockKey, long waitTime, Supplier<T> task) {
        RLock lock = null;
        try {
            // 1. 获取锁对象
            lock = redissonClient.getLock(lockKey);

            // 2. 尝试获取锁:waitTime=等待时间,leaseTime=-1(启用看门狗)
            boolean isLocked = lock.tryLock(waitTime, -1, TimeUnit.MILLISECONDS);
            if (!isLocked) {
                log.warn("获取锁失败,lockKey: {}", lockKey);
                throw new RuntimeException("系统繁忙,请稍后重试");
            }

            // 3. 执行任务(看门狗会自动续期,直到任务完成)
            log.info("获取锁成功,开始执行任务,lockKey: {}", lockKey);
            return task.get();

        } catch (InterruptedException e) {
            log.error("获取锁被中断,lockKey: {}", lockKey, e);
            Thread.currentThread().interrupt(); // 恢复中断状态
            throw new RuntimeException("操作被中断");
        } catch (Exception e) {
            log.error("任务执行异常,lockKey: {}", lockKey, e);
            throw e;
        } finally {
            // 4. 释放锁(必须检查当前线程是否持有锁,避免误释放)
            if (lock != null && lock.isHeldByCurrentThread()) {
                try {
                    lock.unlock();
                    log.info("释放锁成功,lockKey: {}", lockKey);
                } catch (Exception e) {
                    log.error("释放锁失败,lockKey: {}", lockKey, e);
                }
            }
        }
    }
}

5. 业务层使用示例(如订单创建)

import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;

@Service
public class OrderService {

    @Resource
    private RedissonDistributedLock distributedLock;

    /**
     * 创建订单(需要分布式锁防止并发创建)
     */
    public String createOrder(Long userId, BigDecimal amount) {
        // 锁的唯一标识:按用户ID粒度(减少锁竞争)
        String lockKey = "order:create:lock:" + userId;

        // 调用工具类,获取锁并执行任务(最大等待10秒)
        return distributedLock.executeWithLock(lockKey, 10000, () -> {
            // 核心业务逻辑(如检查库存、扣减余额、创建订单)
            // 模拟长任务(超过默认锁过期时间30秒,测试看门狗续期)
            try {
                Thread.sleep(40000); // 执行40秒
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "订单创建成功,userId: " + userId + ", amount: " + amount;
        });
    }
}

三、使用时的注意事项

1. 锁的粒度设计:避免 “大锁” 导致性能瓶颈

  • 错误做法:使用全局锁(如 lock:order),所有订单创建都竞争同一把锁,导致并发性能骤降;
  • 正确做法:按资源 ID 粒度设计锁(如 lock:order:userIdlock:order:productId),仅限制同一资源的并发,减少锁竞争。

2. 看门狗参数配置:根据任务耗时调整 lockWatchdogTimeout

  • 看门狗的默认过期时间是 30 秒,续期间隔 10 秒;若任务平均耗时超过 30 秒,需在配置中增大 lockWatchdogTimeout(如 60 秒),避免续期不及时导致锁提前释放;
  • 配置方式:通过 application.ymlredisson.lockWatchdogTimeoutRedissonConfig 中的 config.setLockWatchdogTimeout(60000) 设置。

3. 锁的释放:必须在 finally 中检查并释放

  • 释放锁前必须通过 lock.isHeldByCurrentThread() 确认当前线程持有锁,避免释放其他线程 / 节点的锁(如线程 A 获取锁后阻塞,线程 B 超时未获取锁却误释放 A 的锁);
  • 释放操作必须放在 finally 块中,确保无论任务成功 / 失败,锁都会被释放(除非节点崩溃,此时看门狗会在锁过期后自动释放)。

4. 避免 “锁泄露”:处理异常和中断

  • 任务执行过程中若发生未捕获的异常,需确保锁能正常释放(finally 块的作用);
  • 若线程被中断(如 InterruptedException),需调用 Thread.currentThread().interrupt() 恢复中断状态,避免上层逻辑忽略中断信号。

5. Redis 高可用:确保 Redis 集群稳定

  • 分布式锁依赖 Redis 的可用性,若 Redis 单点故障,会导致锁机制失效;
  • 生产环境必须部署 Redis 集群(主从 + 哨兵或 Redis Cluster),并开启持久化(AOF+RDB),避免数据丢失导致锁状态异常。

6. 任务幂等性:即使锁失效也能保证结果正确

  • 锁机制无法 100% 避免极端情况(如 Redis 集群脑裂),因此任务本身必须保证幂等性(如通过唯一订单号去重);
  • 示例:创建订单时,先检查订单是否已存在,再执行创建逻辑,避免重复创建。

7. 避免 “锁超时” 与 “任务超时” 不匹配

  • 若手动指定 leaseTime(非 - 1),则看门狗机制不生效,锁会在 leaseTime 后自动释放;此时需确保 leaseTime 远大于任务最大耗时,否则会导致任务未完成锁已释放;
  • 推荐:使用 leaseTime = -1 启用看门狗,由 Redisson 自动管理锁的过期时间。

8. 监控与告警:及时发现锁异常

  • 通过 Redisson 的监控功能(如 JMX)监控锁的持有时间、续期次数,及时发现异常续期;
  • 对 “获取锁失败”“释放锁失败” 等日志配置告警(如钉钉、邮件),快速定位分布式锁问题。

总结

Redisson 的看门狗机制通过自动续期解决了 “长任务锁过期” 的问题,核心代码需注意锁的获取参数(leaseTime = -1)、释放逻辑(finally + isHeldByCurrentThread)、锁粒度设计。实际使用中需结合 Redis 高可用、任务幂等性、监控告警等措施,确保分布式锁在高并发场景下的可靠性。

注意事项

在使用 Redisson 的看门狗(Watch Dog)机制时,默认情况下不需要手动修改锁的过期时间和续期间隔,Redisson 会自动处理。

但如果业务场景有特殊需求(如任务执行时间很长),可以手动调整锁的初始过期时间,续期间隔会自动跟随调整,无需单独配置。

一、默认行为:无需手动干预

Redisson 的看门狗机制有一套默认参数,满足大多数常规场景:

  • 默认锁过期时间(lockWatchdogTimeout:30 秒(30000 毫秒)。
  • 默认续期间隔:锁过期时间的 1/3(即 10 秒)。

当调用 tryLocklock 方法时,若未指定 leaseTime(或设为 -1),看门狗会自动启动:

  • 初始时,锁的过期时间被设置为 30 秒;
  • 后台线程每隔 10 秒(30/3)自动为锁续期,将过期时间重置为 30 秒;
  • 直到线程主动调用 unlock() 释放锁,或线程崩溃(此时看门狗线程终止,锁最终会在 30 秒后过期释放)。

二、何时需要手动修改?

如果业务任务的平均执行时间超过 30 秒(如大数据处理、复杂计算),默认的 30 秒过期时间可能导致续期不及时(极端情况下,任务未完成但锁已过期)。此时需要手动调整 lockWatchdogTimeout(锁过期时间),续期间隔会自动变为 lockWatchdogTimeout / 3,无需单独设置。

三、如何手动修改锁的过期时间?

通过配置文件或代码修改 lockWatchdogTimeout 即可,续期间隔会自动适配。

1. 通过 application.yml 配置(推荐)

redisson:
  lockWatchdogTimeout: 60000  # 设置锁过期时间为 60秒(毫秒)
  singleServerConfig:
    host: 127.0.0.1
    port: 6379
    password: 123456
  • 此时续期间隔会自动变为 60000 / 3 = 20秒,即每隔 20 秒续期一次,将锁过期时间重置为 60 秒。

2. 通过代码配置(RedissonConfig)

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
               .setAddress("redis://127.0.0.1:6379")
               .setPassword("123456");

        // 设置锁过期时间为 60秒(毫秒)
        config.setLockWatchdogTimeout(60000); 

        return Redisson.create(config);
    }
}

四、关键注意事项

  1. 续期间隔无需手动设置:续期间隔固定为 lockWatchdogTimeout / 3,由 Redisson 自动计算,无法单独修改。例如:
    • lockWatchdogTimeout = 90000 毫秒(90 秒),续期间隔为 30 秒。
  2. 手动指定 leaseTime 会禁用看门狗:若调用 tryLock 时指定了具体的 leaseTime(如 tryLock(10, 50, TimeUnit.SECONDS)),则看门狗机制不生效,锁会在 50秒 后强制过期,无论任务是否完成。因此,只有当 leaseTime = -1 时,看门狗才会启用
  3. 根据任务耗时合理设置lockWatchdogTimeout 应大于任务的最大可能执行时间(如任务最长执行 5 分钟,则 lockWatchdogTimeout 可设为 10 分钟),避免极端情况下续期失败导致锁提前释放。
  4. 避免过度调大:若 lockWatchdogTimeout 过大(如几小时),一旦持有锁的线程崩溃,锁会长期占用,影响其他线程获取,需结合业务容错机制(如定时清理异常锁)。

总结

  • 默认场景:无需手动修改,使用 Redisson 内置的 30 秒过期时间和 10 秒续期间隔即可。
  • 长任务场景:通过 lockWatchdogTimeout 调整锁过期时间(如 60 秒),续期间隔会自动变为其 1/3,无需单独配置。
  • 核心原则lockWatchdogTimeout 需大于任务最大耗时,且不指定 leaseTime(保持 -1)以启用看门狗。
SpringBoot
知识点
Redis