Redisson 的 Watchdog 机制

为避免 Redis 实现的分布式锁超时问题,Redisson 引入了 Watchdog 机制。该机制能在 Redisson 实例关闭前持续延长锁的有效期。

主要功能

  1. 自动续租:当客户端获取未指定超时时间的锁时,Watchdog会基于Netty时间轮启动后台任务,定期(默认每10秒)将锁的过期时间重置为30秒(默认租约时间的1/3)。
  2. 续期控制:锁释放或客户端关闭时自动停止续租。

实现原理

Watchdog的核心逻辑位于scheduleExpirationRenewal方法:

java 复制代码
protected 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);
        try {
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

// 定时续期任务
private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) return;
    
    Timeout task = getServiceManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) return;
            
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) return;
            
            CompletionStage<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock {} expiration", getRawName(), e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                
                if (res) {
                    renewExpiration(); // 续期成功后重新调度
                } else {
                    cancelExpirationRenewal(null);
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

// 使用LUA脚本续期
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

关键实现点

  • 通过TimerTask定时执行续期任务,默认每10秒(30s/3)执行一次
  • 使用LUA脚本完成续期操作,将锁重新设为30秒
  • 续期前会检查EXPIRATION_RENEWAL_MAP中是否存在对应entry,不存在则停止续期

锁释放时的处理逻辑:

java 复制代码
@Override
public void unlock() {
    try {
        get(unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException e) {
        if (e.getCause() instanceof IllegalMonitorStateException) {
            throw (IllegalMonitorStateException) e.getCause();
        }
        throw e;
    }
}

@Override
public RFuture<Void> unlockAsync(long threadId) {
    return getServiceManager().execute(() -> unlockAsync0(threadId));
}

private RFuture<Void> unlockAsync0(long threadId) {
    CompletionStage<Boolean> future = unlockInnerAsync(threadId);
    CompletionStage<Void> f = future.handle((opStatus, e) -> {
        cancelExpirationRenewal(threadId);
        // 异常处理逻辑...
        return null;
    });
    return new CompletableFutureWrapper<>(f);
}

protected void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) return;
    
    if (threadId != null) {
        task.removeThreadId(threadId);
    }

    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) timeout.cancel();
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}

解锁流程

  1. 调用unlockAsync方法
  2. 最终执行cancelExpirationRenewal移除EXPIRATION_RENEWAL_MAP中的entry
  3. 确保后续不会继续续期

续期触发条件

Redisson创建分布式锁时,并非所有情况都会触发续期机制。通过分析加锁过程的代码实现可以了解续期触发的具体条件:

java 复制代码
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime > 0) {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);
    ttlRemainingFuture = new CompletableFutureWrapper<>(s);

    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
        // 成功获取锁
        if (ttlRemaining == null) {
            if (leaseTime > 0) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
}

重点关注第15-19行代码:仅当leaseTime <= 0时,Redisson才会触发续期机制。因此,如果在加锁时明确指定了超时时间,则不会进行自动续期。

续期终止条件

终止条件一:解锁操作

当调用锁的unlock方法时,续期机制会自动终止。核心终止逻辑如下:

java 复制代码
protected void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }
    
    if (threadId != null) {
        task.removeThreadId(threadId);
    }

    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) {
            timeout.cancel();
        }
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}

主要通过EXPIRATION_RENEWAL_MAP.remove操作实现终止。

终止条件二:线程中断

续期机制还可能因线程中断而终止:

java 复制代码
protected 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);
        try {
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

在初始化续期过程中,如果线程被中断,则会自动取消续期操作。

续期机制说明

  1. Redisson当前未设置最大续期次数和最长续期时间的限制。正常情况下,如果未执行解锁操作,续期将持续进行。

  2. 续期机制基于Netty的时间轮(TimerTask、Timeout、Timer)实现,所有操作都在JVM层面执行。当应用发生宕机、下线或重启时,续期任务会自动终止,这在一定程度上可以避免因机器故障导致的锁长期不释放问题。

解锁失败,watchdog会不会一直续期下去

不会

相关推荐
JIngJaneIL16 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
微学AI17 小时前
复杂时序场景的突围:金仓数据库是凭借什么超越InfluxDB?
数据库
廋到被风吹走17 小时前
【数据库】【Redis】定位、优势、场景与持久化机制解析
数据库·redis·缓存
有想法的py工程师18 小时前
PostgreSQL + Debezium CDC 踩坑总结
数据库·postgresql
Nandeska18 小时前
2、数据库的索引与底层数据结构
数据结构·数据库
小卒过河010419 小时前
使用apache nifi 从数据库文件表路径拉取远程文件至远程服务器目的地址
运维·服务器·数据库
过期动态19 小时前
JDBC高级篇:优化、封装与事务全流程指南
android·java·开发语言·数据库·python·mysql
Mr.朱鹏19 小时前
SQL深度分页问题案例实战
java·数据库·spring boot·sql·spring·spring cloud·kafka
一位代码19 小时前
mysql | 常见日期函数使用及格式转换方法
数据库·mysql
SelectDB19 小时前
Apache Doris 4.0.2 版本正式发布
数据库·人工智能