分析:
首先,需要区分普通数据操作和分布式锁场景的不同需求。普通的 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
命令(允许覆盖)是由数据存储的更新需求决定的。
set
命令的 “新增 / 更新统一性”普通场景下(如存储用户信息、商品数据),set key value
命令的设计目标是 “无论 key 是否存在,都能将 value 写入 Redis”:
key 不存在
时,set
是 “新增” 操作,Redis 会创建该 key 并存储 value;key 已存在
时,set
是 “更新” 操作,Redis 会用新 value 覆盖旧 value。这种设计的原因是:普通数据存储需要支持 “修改” 操作。例如,用户修改昵称时,需要用新昵称覆盖旧昵称,此时必须允许 set
命令覆盖已有 key。
set
命令的 “互斥性需求”Redisson 的看门狗机制是为分布式锁服务的,而分布式锁的核心是 “互斥性”—— 同一时间只能有一个线程持有锁。
因此,锁的初始获取必须满足:只有当锁 key 不存在时,才能成功设置(即只有第一个线程能获取锁)。
具体来说,Redisson 在获取锁时执行的 set
命令是带条件的:
# 伪代码:Redisson 获取锁时的 Redis 命令
SET lock_key [唯一标识] NX PX [初始过期时间]
其中:
NX
(Not Exist):仅当 key 不存在时才执行设置,若 key 已存在则直接返回失败(nil
);PX [初始过期时间]
:设置锁的初始过期时间(如 30 秒,看门狗会在此基础上续期);[唯一标识]
:Redisson 生成的随机字符串(关联当前线程),用于标识锁的持有者。NX
条件?(核心区别)分布式锁的核心是 “互斥”,即同一时间只能有一个线程持有锁。如果不用 NX
条件(即允许覆盖已有 key),会导致:
lock_key
);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
命令,而是通过验证持有者身份后更新过期时间,与初始锁的获取逻辑完全不同。Redisson 的看门狗(Watch Dog)机制是分布式锁的核心特性之一,其核心作用是自动为持有锁的线程续期,避免因任务执行时间过长导致锁提前释放(从而引发并发问题)。下面通过具体代码实现,并详细说明使用时的注意事项。
Redisson 的看门狗机制触发条件:
使用tryLock
或lock
方法时,
需要未指定锁的过期时间,即不能手动指定过期时间(或者将leaseTime = -1
),
Redisson 会自动启动后台线程(看门狗),每隔 lockWatchdogTimeout / 3
时间(默认 10 秒)为锁续期,将锁的过期时间重置为 lockWatchdogTimeout
(默认 30 秒)。
直到线程主动释放锁或崩溃(此时看门狗线程终止,锁最终会过期释放)。
<!-- 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>
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 # 命令执行超时时间(毫秒)
若需自定义 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);
}
}
封装分布式锁的获取、释放逻辑,简化业务层调用:
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);
}
}
}
}
}
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;
});
}
}
lock:order
),所有订单创建都竞争同一把锁,导致并发性能骤降;lock:order:userId
或 lock:order:productId
),仅限制同一资源的并发,减少锁竞争。lockWatchdogTimeout
lockWatchdogTimeout
(如 60 秒),避免续期不及时导致锁提前释放;application.yml
的 redisson.lockWatchdogTimeout
或 RedissonConfig
中的 config.setLockWatchdogTimeout(60000)
设置。finally
中检查并释放lock.isHeldByCurrentThread()
确认当前线程持有锁,避免释放其他线程 / 节点的锁(如线程 A 获取锁后阻塞,线程 B 超时未获取锁却误释放 A 的锁);finally
块中,确保无论任务成功 / 失败,锁都会被释放(除非节点崩溃,此时看门狗会在锁过期后自动释放)。finally
块的作用);InterruptedException
),需调用 Thread.currentThread().interrupt()
恢复中断状态,避免上层逻辑忽略中断信号。leaseTime
(非 - 1),则看门狗机制不生效,锁会在 leaseTime
后自动释放;此时需确保 leaseTime
远大于任务最大耗时,否则会导致任务未完成锁已释放;leaseTime = -1
启用看门狗,由 Redisson 自动管理锁的过期时间。Redisson 的看门狗机制通过自动续期解决了 “长任务锁过期” 的问题,核心代码需注意锁的获取参数(leaseTime = -1
)、释放逻辑(finally + isHeldByCurrentThread
)、锁粒度设计。实际使用中需结合 Redis 高可用、任务幂等性、监控告警等措施,确保分布式锁在高并发场景下的可靠性。
在使用 Redisson 的看门狗(Watch Dog)机制时,默认情况下不需要手动修改锁的过期时间和续期间隔,Redisson 会自动处理。
但如果业务场景有特殊需求(如任务执行时间很长),可以手动调整锁的初始过期时间,续期间隔会自动跟随调整,无需单独配置。
Redisson 的看门狗机制有一套默认参数,满足大多数常规场景:
lockWatchdogTimeout
):30 秒(30000 毫秒)。当调用 tryLock
或 lock
方法时,若未指定 leaseTime
(或设为 -1
),看门狗会自动启动:
unlock()
释放锁,或线程崩溃(此时看门狗线程终止,锁最终会在 30 秒后过期释放)。如果业务任务的平均执行时间超过 30 秒(如大数据处理、复杂计算),默认的 30 秒过期时间可能导致续期不及时(极端情况下,任务未完成但锁已过期)。此时需要手动调整 lockWatchdogTimeout
(锁过期时间),续期间隔会自动变为 lockWatchdogTimeout / 3
,无需单独设置。
通过配置文件或代码修改 lockWatchdogTimeout
即可,续期间隔会自动适配。
application.yml
配置(推荐)redisson:
lockWatchdogTimeout: 60000 # 设置锁过期时间为 60秒(毫秒)
singleServerConfig:
host: 127.0.0.1
port: 6379
password: 123456
60000 / 3 = 20秒
,即每隔 20 秒续期一次,将锁过期时间重置为 60 秒。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);
}
}
lockWatchdogTimeout / 3
,由 Redisson 自动计算,无法单独修改。例如:
lockWatchdogTimeout = 90000
毫秒(90 秒),续期间隔为 30 秒。leaseTime
会禁用看门狗:若调用 tryLock
时指定了具体的 leaseTime
(如 tryLock(10, 50, TimeUnit.SECONDS)
),则看门狗机制不生效,锁会在 50秒
后强制过期,无论任务是否完成。因此,只有当 leaseTime = -1
时,看门狗才会启用。lockWatchdogTimeout
应大于任务的最大可能执行时间(如任务最长执行 5 分钟,则 lockWatchdogTimeout
可设为 10 分钟),避免极端情况下续期失败导致锁提前释放。lockWatchdogTimeout
过大(如几小时),一旦持有锁的线程崩溃,锁会长期占用,影响其他线程获取,需结合业务容错机制(如定时清理异常锁)。lockWatchdogTimeout
调整锁过期时间(如 60 秒),续期间隔会自动变为其 1/3,无需单独配置。lockWatchdogTimeout
需大于任务最大耗时,且不指定 leaseTime
(保持 -1
)以启用看门狗。lock.tryLock(0, 30000L, TimeUnit.MILLISECONDS)
tryLock方法,这里的tryLock(waitTime, -1, unit)有三个参数:
waiTime
参数设置为 0,只抢一次,抢不到就放弃。leaseTime 不等于-1 就说明没有启动看门狗机制,那么执行的是正常获取锁的操作。