Redis的新增修改和Redisson的看门狗机制


Redis 新增和更新数据的方法都是set,为什么redisson实现看门狗机制时,仅当 key 不存在时才会设置成功,若 key 已存在则设置失败。

有什么区别?

分析:

首先,需要区分普通数据操作和分布式锁场景的不同需求。普通的 set 操作(比如存储用户信息)需要覆盖已有值,所以用不带条件的 set,这时候新增和更新是同一个操作。而分布式锁的核心是互斥性,必须保证同一时间只有一个线程能获取锁,所以需要确保锁的 key 只有在不存在时才能被设置,这时候需要带 NX 参数的 set 命令。

然后,Redisson 的看门狗机制是为了分布式锁服务的。分布式锁的获取必须是原子操作,通过 set key value NX PX ... 来实现,这样只有第一个线程能成功设置 key,后续线程因为 key 已存在而失败,保证互斥。而看门狗的作用是在锁未释放时自动续期,防止锁过期,这时候续期操作是针对已存在的锁 key(属于当前线程持有),所以续期时用的是另一个命令(比如通过 Lua 脚本更新过期时间),而不是初始的 set 命令。

需要解释清楚:普通 set 用于数据存储,允许覆盖,所以不需要条件;分布式锁的初始获取需要互斥,所以用带 NX 的 set,确保只有一个线程成功;而看门狗的续期是针对已持有的锁,用的是更新过期时间的操作,和初始的 set 不同。

还要说明 Redisson 中 tryLock 方法的初始锁获取用的是 set NX,而续期用的是另一个原子操作(比如 PEXPIRE 或 Lua 脚本),这样既保证了初始获取的互斥性,又能在持有锁时更新过期时间,实现自动续期。

总结起来,区别在于使用场景和目的不同:普通 set 是为了存储 / 更新数据,允许覆盖;分布式锁的初始 set 带 NX 是为了互斥,确保只有一个持有者;看门狗的续期是更新已有锁的过期时间,和初始 set 的逻辑不同。

要理解这个问题,需要区分两个核心场景的本质差异:

  • 普通数据的存储 / 更新

  • 分布式锁的获取

    Redisson 的看门狗机制是为分布式锁设计的,其对 set 命令的使用方式(key不存在时才成功)是由分布式锁的互斥性需求决定的,而普通 Redis 数据操作的 set 命令(允许覆盖)是由数据存储的更新需求决定的。

一、普通 Redis 数据操作:set 命令的 “新增 / 更新统一性”

普通场景下(如存储用户信息、商品数据),set key value 命令的设计目标是 “无论 key 是否存在,都能将 value 写入 Redis”

  • key 不存在 时,set 是 “新增” 操作,Redis 会创建该 key 并存储 value;
  • key 已存在 时,set 是 “更新” 操作,Redis 会用新 value 覆盖旧 value。

这种设计的原因是:普通数据存储需要支持 “修改” 操作。例如,用户修改昵称时,需要用新昵称覆盖旧昵称,此时必须允许 set 命令覆盖已有 key。

二、Redisson 分布式锁(带看门狗):set 命令的 “互斥性需求”

Redisson 的看门狗机制是为分布式锁服务的,而分布式锁的核心是 “互斥性”—— 同一时间只能有一个线程持有锁。

因此,锁的初始获取必须满足:只有当锁 key 不存在时,才能成功设置(即只有第一个线程能获取锁)

具体来说,Redisson 在获取锁时执行的 set 命令是带条件的

# 伪代码:Redisson 获取锁时的 Redis 命令
SET lock_key [唯一标识] NX PX [初始过期时间]

其中:

  • NX(Not Exist):仅当 key 不存在时才执行设置,若 key 已存在则直接返回失败(nil);
  • PX [初始过期时间]:设置锁的初始过期时间(如 30 秒,看门狗会在此基础上续期);
  • [唯一标识]:Redisson 生成的随机字符串(关联当前线程),用于标识锁的持有者。

为什么必须用 NX 条件?(核心区别)

分布式锁的核心是 “互斥”,即同一时间只能有一个线程持有锁。如果不用 NX 条件(即允许覆盖已有 key),会导致:

  • 线程 A 先获取锁(创建 lock_key);
  • 线程 B 可以直接用 set 命令覆盖 lock_key,导致线程 A 的锁被 “偷取”,互斥性失效。

因此,NX 条件是分布式锁互斥性的根本保证 —— 只有第一个线程能成功设置锁 key,后续线程会因 key 已存在 而失败,无法获取锁。

三、看门狗的 “续期操作” 与初始 set 命令的区别

需要注意:

看门狗的续期操作并非使用 set 命令,而是单独的 “更新过期时间” 操作

当线程成功获取锁(set 命令返回 OK)后,看门狗会启动后台线程,每隔 10 秒(默认)执行一次续期命令,将锁的过期时间重置为 30 秒(默认)。

续期命令通常是通过 Lua 脚本实现的原子操作

-- 伪代码:看门狗续期的 Lua 脚本
if redis.call('get', KEYS[1]) == ARGV[1] then  -- 检查当前锁是否由自己持有(唯一标识匹配)
    return redis.call('pexpire', KEYS[1], ARGV[2])  -- 延长过期时间
else
    return 0  -- 若锁已被其他线程持有,续期失败
end

这个过程的核心是:

  • 续期操作只针对当前线程已持有的锁(通过唯一标识验证);
  • 续期不创建新锁,只更新已有锁的过期时间;
  • 这与初始获取锁的 set NX 命令完全不同(初始是 “创建锁”,续期是 “更新已有锁的过期时间”)。

四、总结:两种 set 用法的核心区别

场景set 命令用法目的关键参数
普通数据存储 / 更新set key value(无 NX存储或覆盖数据,支持修改操作无特殊条件
分布式锁初始获取set key value NX PX [时间]保证互斥性,仅允许第一个线程获取锁NX(互斥核心)

简言之:

  • 普通 set 是为了 “存储 / 更新数据”,所以允许覆盖;
  • 分布式锁的初始 set 是为了 “抢占锁”,所以必须用 NX 保证只有一个线程能成功,这是互斥性的基础;
  • 看门狗的续期操作不依赖 set 命令,而是通过验证持有者身份后更新过期时间,与初始锁的获取逻辑完全不同。

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)以启用看门狗。

lock.tryLock(0, 30000L, TimeUnit.MILLISECONDS)

tryLock方法,这里的tryLock(waitTime, -1, unit)有三个参数:

  • waitTime:获取锁的最大等待时间(没有传默认为-1)指某个线程在获取锁时最多等待的时间,超过时间没获取到就不等了
  • leaseTime:锁自动释放的时间(没有传的话默认-1) 指某个线程获取到锁后,到释放锁的时间,也就是指定该线程可以持有该锁的时长。
  • unit:时间的单位(等待时间和锁自动释放的时间单位)
  1. 因为定时任务只执行一次,锁的等待时间waiTime参数设置为 0,只抢一次,抢不到就放弃。
  2. 注意锁的释放一定要写在 finally 中。
  3. leaseTime 设置为 -1 ,表示不给锁手动设置指定的过期时间,交给 redisson 自动管理锁到期和续期。默认的续期时间是 30s,并且方法没执行完会自动续期。
    • [启动看门狗机制,必须将leaseTime设置为-1.]
    • leaseTime 不等于-1 就说明没有启动看门狗机制,那么执行的是正常获取锁的操作。

SpringBoot
JAVA-技能点
知识点
Redis