Redisson 的 WatchDog 机制:分布式锁超时难题的终极解决方案
mhr18 2025-07-28 18:17 17 浏览 0 评论
你是不是也遇到过这样的情况:在分布式系统里用 Redisson 加了锁,业务还没处理完,锁就自己释放了,紧接着并发问题就找上门来?这时候,WatchDog 机制就像个默默守护的 “锁匠”,只是很多开发者对它的工作原理还一知半解。今天咱们就掰开揉碎了聊聊,这个藏在 Redisson 里的 “守护者” 到底是怎么干活的,从源码到实战,一次性讲透。
为什么需要 WatchDog?
在互联网项目里,分布式锁就像十字路口的红绿灯,要是红绿灯突然失灵,各种线程、进程就会乱成一锅粥。你想想,秒杀场景里的库存扣减、支付系统的订单处理,哪一个离得开可靠的分布式锁?
可实际开发中,业务逻辑的执行时间往往没法精准预估 —— 网络延迟、数据量暴增、第三方接口卡顿,随便一个因素都可能让业务处理超时。这时候如果锁准时释放,其他线程趁机抢锁,数据一致性就成了泡影。比如在一个电商平台的下单流程中,假设给锁设置了 30 秒超时,可某次因为数据库压力过大,订单处理花了 40 秒,当第 30 秒锁释放后,另一个线程获取到锁进行下单操作,就可能导致超卖。
早期解决这个问题,不少团队会采取 “笨办法”:把超时时间设得特别长。但这就像为了防止门被风吹关,故意把门锁拆了一样危险。一旦持有锁的线程意外挂掉,这把 “大锁” 就会一直占着资源,其他线程只能干等着,系统性能直接跳水。曾有一个社交平台,因线程挂掉导致锁长期未释放,使得消息推送功能瘫痪了 2 小时,造成了巨大的用户流失。
而 Redisson 的 WatchDog 机制,就是为了在 “锁超时释放” 和 “线程挂掉后锁不释放” 这两个坑之间,搭一座安全的桥。
WatchDog 的 3 大核心能力
自动续期
当你用 Redisson 的RLock加锁时,如果没指定超时时间,WatchDog 就会自动启动。它会每隔 30 秒(这个时间可以通过lockWatchdogTimeout参数调整)给锁续一次期,每次续期都会把锁的有效期重置为 30 秒。
从源码来看,Redisson 在RedissonLock类的lock方法中,会判断如果没有指定超时时间,就会调用scheduleExpirationRenewal方法开启 WatchDog 的续期任务。以下是关键源码片段:
// RedissonLock类中的lock方法
@Override
public void lock() {
try {
lock(-1, null, false);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
// 省略部分代码...
if (leaseTime == -1) {
// 未指定超时时间时,启动WatchDog续期
this.leaseTime = internalLockLeaseTime;
scheduleExpirationRenewal(threadId);
} else {
// 指定了超时时间,不启动WatchDog
this.leaseTime = unit.toMillis(leaseTime);
}
}
scheduleExpirationRenewal方法会创建一个定时任务,通过Timer来实现每隔lockWatchdogTimeout/3的时间(默认 10 秒)检查一次,如果锁还被当前线程持有,就会发送PEXPIRE命令给 Redis,将锁的有效期延长至 30 秒。续期核心代码如下:
// 续期任务的核心逻辑
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
// 启动定时续期任务
renewExpiration();
}
}
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 创建定时任务,每隔lockWatchdogTimeout/3时间执行一次
TimerTask task = new TimerTask() {
@Override
public void run() {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 发送续期命令到Redis
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// 续期成功,再次调度续期任务
renewExpiration();
}
});
}
};
ee.setTimeout(timer.newTimeout(task, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS));
}
这就好比给锁上了个闹钟,只要业务还在正常运行,闹钟就会不断往后拨,直到业务处理完,咱们主动调用unlock方法,WatchDog 才会调用cancelExpirationRenewal方法取消续期任务。
智能判断:只给 “合法持有者” 续期
WatchDog 可不是盲目续期的,它只会给当前持有锁的线程续期。Redisson 内部通过ThreadLocal记录了锁的持有者信息,每次续期前都会校验当前线程是不是合法持有者,避免给 “抢锁者” 续期的乌龙情况。
在RedissonLock的renewExpiration方法中,会先通过ThreadLocal获取当前线程持有的锁的 ID,然后与 Redis 中存储的锁的 ID 进行比对,如果一致才会进行续期操作。关键校验逻辑源码如下:
// 续期前的线程合法性校验
private RFuture<Boolean> renewExpirationAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 校验当前线程是否为锁的持有者
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 续期操作,设置锁的有效期为30秒
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
其中,ARGV[2]是当前线程持有的锁 ID,通过getLockName(threadId)方法生成,而redis.call('hexists', KEYS[1], ARGV[2])用于判断该锁 ID 是否存在于 Redis 的哈希结构中,以此验证线程的合法性。这就像小区保安只给登记过的业主开门,外人想浑水摸鱼根本没门。
应急刹车:线程挂了,锁也能 “自动过期”
如果持有锁的线程因为异常终止了,WatchDog 的续期任务也会跟着停掉。这时候锁的有效期会在剩余时间后自动结束,其他线程就能正常抢锁了,不会出现资源长期被占的情况。
这是因为续期任务是与当前线程绑定的,当线程终止时,对应的定时任务也会被取消。Redisson 在unlock方法中会清理线程相关的续期任务,源码如下:
// 释放锁时取消续期任务
@Override
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
@Override
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<>();
// 发送解锁命令到Redis
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.whenComplete((opStatus, e) -> {
// 取消续期任务
cancelExpirationRenewal(threadId);
if (e != null) {
result.tryFailure(e);
return;
}
// 省略部分代码...
});
return result;
}
// 取消续期任务的核心方法
void cancelExpirationRenewal(Long threadId) {
ExpirationEntry entry = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (entry == null) {
return;
}
if (threadId != null) {
entry.removeThreadId(threadId);
if (entry.hasNoThreads()) {
// 线程全部终止,取消定时任务
Timeout timeout = entry.getTimeout();
if (timeout != null) {
timeout.cancel();
}
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
} else {
// 线程为null,直接清理所有续期任务
entry.clear();
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
而且 Redis 中的锁本身就有过期时间,一旦续期停止,锁就会在过期后自动释放。这种 “有始有终” 的设计,完美平衡了安全性和灵活性。
开发必知:WatchDog 的 “启用规则” 和 “避坑指南”
1. 什么时候 WatchDog 会工作?
只有当你调用lock()方法且不指定超时时间时,WatchDog 才会启动;如果用了lock(long leaseTime, TimeUnit unit)指定了超时时间,WatchDog 就会 “休假”,这时候锁会严格按照你设置的时间释放。
从源码分析,当调用带超时时间的lock方法时,Redisson 会直接将锁的过期时间设置为指定的时间,而不会启动scheduleExpirationRenewal方法。所以在业务时间不确定的场景下,最好别指定超时时间,把续期的活儿交给 WatchDog。
比如在处理一个数据量不确定的批量任务时,由于无法预估具体的处理时间,使用无参的lock方法,让 WatchDog 自动续期是更稳妥的选择。
2. 集群环境下的注意事项
WatchDog 的续期操作依赖于和 Redis 的正常通信,如果 Redis 主节点挂了,虽然 Redisson 有哨兵或集群模式保障,但切换期间可能会出现短暂的续期延迟。
这时候建议搭配 Redisson 的retryInterval和retryAttempts参数,让客户端在通信失败时多做几次尝试,提高续期成功率。同时,在 Redis 集群环境中,要确保lockWatchdogTimeout的设置大于 Redis 主从切换的最大可能时间,避免在切换过程中锁过期。
另外,要注意避免在锁持有期间进行长时间的阻塞操作,比如等待用户输入等,这可能会导致 WatchDog 的续期任务无法正常执行,从而引发锁过期的问题。
3. 实战案例:一次因 WatchDog 配置不当导致的事故
有一个电商平台,在使用 Redisson 的 WatchDog 机制时,将lockWatchdogTimeout设置成了 60 秒,而实际业务中,有部分流程需要 80 秒才能完成。结果在高并发场景下,出现了锁提前释放的情况,导致商品超卖。
后来排查发现,由于lockWatchdogTimeout设置过大,续期的间隔也相应变长(60/3=20 秒),当业务执行到第 60 秒时,锁的初始有效期已到,而续期任务要到第 80 秒才执行,此时锁已过期,其他线程趁机抢锁导致并发问题。这就提醒我们,lockWatchdogTimeout的设置要根据业务的平均执行时间来合理调整,一般建议设置为业务平均执行时间的 1.5-2 倍。
与其他分布式锁续期机制的对比
除了 Redisson 的 WatchDog 机制,还有一些其他的分布式锁续期方式,比如基于 ZooKeeper 的分布式锁,可以通过会话的临时节点来实现自动释放,当客户端与 ZooKeeper 的连接断开时,临时节点会被删除,锁也随之释放。但与 WatchDog 机制相比,ZooKeeper 的方式在续期的灵活性上稍逊一筹,WatchDog 可以根据业务的执行情况动态调整续期的时间。
另外,还有一些基于数据库实现的分布式锁,通常需要通过定时任务来手动续期,这不仅增加了开发成本,而且续期的可靠性也不如 WatchDog 机制,容易出现定时任务失败导致锁过期的问题。
总结
说到底,WatchDog 机制解决的不仅是技术问题,更是咱们开发中的 “安全感” 问题。它让分布式锁从 “定时炸弹” 变成了 “智能警卫”,既不会提前离岗,也不会霸占岗位。
如果你之前在分布式锁上栽过跟头,不妨试试让 WatchDog 来把关;如果已经在使用了,也可以回头检查下配置参数是否合理,看看源码中关于 WatchDog 的实现,加深对它的理解。
最后想问问大家,你们团队在使用 Redisson 时,有没有遇到过 WatchDog 相关的坑?是怎么解决的?欢迎在评论区分享你的经验,咱们一起把分布式锁用得更稳、更溜!
相关推荐
- 订单超时自动取消业务的 N 种实现方案,从原理到落地全解析
-
在分布式系统架构中,订单超时自动取消机制是保障业务一致性的关键组件。某电商平台曾因超时处理机制缺陷导致日均3000+订单库存锁定异常,直接损失超50万元/天。本文将从技术原理、实现细节、...
- 使用Spring Boot 3开发时,如何选择合适的分布式技术?
-
作为互联网大厂的后端开发人员,当你满怀期待地用上SpringBoot3,准备在项目中大显身手时,却发现一个棘手的问题摆在面前:面对众多分布式技术,究竟该如何选择,才能让SpringBoot...
- 数据库内存爆满怎么办?99%的程序员都踩过这个坑!
-
你的数据库是不是又双叒叕内存爆满了?!服务器监控一片红色警告,老板在群里@所有人,运维同事的电话打爆了手机...这种场景是不是特别熟悉?别慌!作为一个在数据库优化这条路上摸爬滚打了10年的老司机,今天...
- springboot利用Redisson 实现缓存与数据库双写不一致问题
-
使用了Redisson来操作Redis分布式锁,主要功能是从缓存和数据库中获取商品信息,以下是针对并发时更新缓存和数据库带来不一致问题的解决方案1.基于读写锁和删除缓存策略在并发更新场景下,...
- 外贸独立站数据库炸了?对象缓存让你起死回生
-
上周黑五,一个客户眼睁睁看着服务器CPU飙到100%——每次页面加载要查87次数据库。这让我想起2024年Pantheon的测试:Redis缓存能把WooCommerce查询速度提升20倍。跨境电商最...
- 手把手教你在 Spring Boot3 里纯编码实现自定义分布式锁
-
为什么要自己实现分布式锁?你是不是早就受够了引入各种第三方依赖时的繁琐?尤其是分布式锁这块,每次集成Redisson或者Zookeeper,都得额外维护一堆配置,有时候还会因为版本兼容问题头疼半...
- 如何设计一个支持百万级实时数据推送的WebSocket集群架构?
-
面试解答:要设计一个支持百万级实时数据推送的WebSocket集群架构,需从**连接管理、负载均衡、水平扩展、容灾恢复**四个维度切入:连接层设计-**长连接优化**:采用Netty或Und...
- Redis数据结构总结——面试最常问到的知识点
-
Redis作为主流的nosql存储,面试时经常会问到。其主要场景是用作缓存,分布式锁,分布式session,消息队列,发布订阅等等。其存储结构主要有String,List,Set,Hash,Sort...
- skynet服务的缺陷 lua死循环
-
服务端高级架构—云风的skynet这边有一个关于云风skynet的视频推荐给大家观看点击就可以观看了!skynet是一套多人在线游戏的轻量级服务端框架,使用C+Lua开发。skynet的显著优点是,...
- 七年Java开发的一路辛酸史:分享面试京东、阿里、美团后的心得
-
前言我觉得有一个能够找一份大厂的offer的想法,这是很正常的,这并不是我们的饭后谈资而是每个技术人的追求。像阿里、腾讯、美团、字节跳动、京东等等的技术氛围与技术规范度还是要明显优于一些创业型公司...
- mysql mogodb es redis数据库之间的区别
-
1.MySQL应用场景概念:关系型数据库,基于关系模型,使用表和行存储数据。优点:支持ACID事务,数据具有很高的一致性和完整性。缺点:垂直扩展能力有限,需要分库分表等方式扩展。对于复杂的查询和大量的...
- redis,memcached,nginx网络组件
-
1.理解阻塞io,非阻塞io,同步io,异步io的区别2.理解BIO和AIO的区别io多路复用只负责io检测,不负责io操作阻塞io中的write,能写多少是多少,只要写成功就返回,譬如准备写500字...
- SpringBoot+Vue+Redis实现验证码功能
-
一个小时只允许发三次验证码。一次验证码有效期二分钟。SpringBoot整合Redis...
- AWS MemoryDB 可观测最佳实践
-
AWSMemoryDB介绍AmazonMemoryDB是一种完全托管的、内存中数据存储服务,专为需要极低延迟和高吞吐量的应用程序而设计。它与Redis和Memcached相似,但具有更...
- 从0构建大型AI推荐系统:实时化引擎从工具到生态的演进
-
在AI浪潮席卷各行各业的今天,推荐系统正从幕后走向前台,成为用户体验的核心驱动力。本文将带你深入探索一个大型AI推荐系统从零起步的全过程,揭示实时化引擎如何从单一工具演进为复杂生态的关键路径。无论你是...
你 发表评论:
欢迎- 一周热门
-
-
Redis客户端 Jedis 与 Lettuce
-
高并发架构系列:Redis并发竞争key的解决方案详解
-
redis如何防止并发(redis如何防止高并发)
-
Java SE Development Kit 8u441下载地址【windows版本】
-
开源推荐:如何实现的一个高性能 Redis 服务器
-
redis安装与调优部署文档(WinServer)
-
Redis 入门 - 安装最全讲解(Windows、Linux、Docker)
-
一文带你了解 Redis 的发布与订阅的底层原理
-
Redis如何应对并发访问(redis控制并发量)
-
Oracle如何创建用户,表空间(oracle19c创建表空间用户)
-
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (83)
- php redis (97)
- redis 存储 (67)
- redis 锁 (74)
- 启动 redis (73)
- redis 时间 (60)
- redis 删除 (69)
- redis内存 (64)
- redis并发 (53)
- redis 主从 (71)
- redis同步 (53)
- redis结构 (53)
- redis 订阅 (54)
- redis 登录 (62)
- redis 面试 (58)
- redis问题 (54)
- 阿里 redis (67)
- redis的缓存 (57)
- lua redis (59)
- redis 连接池 (64)