【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁

引言

在上篇中,我们梳理了redisson的可重入锁的加锁流程,而加锁必然就会有锁续期的问题,那么看门狗机制是维持锁续期的关键。因此,在本篇中我们将剖析redisson中的看门狗机制究竟是如何实现的。

利用Watchdog机制异步维持客户端锁

看门狗机制是redisson解决锁续期问题而设置的,在前文中我们也有手写过,这里我们看看"正版"的是如何执行的。

1.首先,在获取锁时会触发看门狗机制:

java 复制代码
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
    Long ttl = null;
    // 如果未指定租约时间,则使用看门狗机制
    if (leaseTime != -1) {
        ttl = tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        // 使用默认的看门狗超时时间(默认30秒)
        ttl = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        // 启动看门狗
        scheduleExpirationRenewal(threadId);
    }
    return ttl;
}

2.看门狗启动逻辑:

java 复制代码
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() {
    // 创建异步续期任务
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ee == null) {
                return;
            }
            
            // 通过Lua脚本延长锁的过期时间
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((success, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (success) {
                    // 如果续期成功,递归调用,实现循环续期
                    renewExpiration();
                }
            });
        }
        // 续期间隔为看门狗超时时间的1/3
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

3.续期的核心 Lua 脚本:

java 复制代码
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), 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(getName()),
        internalLockLeaseTime, // 默认30秒
        getLockName(threadId));
}

4.锁释放时清理看门狗:

java 复制代码
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()) {
        // 取消定时任务
        task.getTimeout().cancel();
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}

让我们总结一下看门狗机制的工作流程:

  • 触发条件 :
    • 当调用 lock() 方法且未指定租约时间时
    • 默认使用 30 秒的看门狗超时时间
  • 核心原理 :
    • 在获取锁成功后,创建一个定时任务
    • 定时任务每隔 10 秒(默认超时时间的 1/3)执行一次
    • 通过 Lua 脚本检查锁是否还存在并延长过期时间
  • 续期策略 :
    • 每次续期都将过期时间刷新为 30 秒
    • 只要业务线程还持有锁,就会一直续期
    • 续期操作是异步的,不会阻塞业务线程
  • 自动清理 :
    • 当锁释放时,自动取消续期任务
    • 通过 EXPIRATION_RENEWAL_MAP 管理所有续期任务
  • 安全保证 :
    • 使用 Lua 脚本保证续期操作的原子性
    • 只有当前持有锁的线程才能续期成功
    • 如果业务异常导致线程中断,看门狗也会停止续期

另外,通过学习源码,我们要正确使用看门狗机制,我们需要注意:不要使用带超时参数的加锁方法,否则看门狗机制不会生效;确保业务执行时间不会过长,否则会产生大量续期操作;记得在完成业务后及时释放锁,避免资源浪费。

小结

看门狗机制的关键在于定时对锁的状态进行检查,回想下我们当时手撸是通过循环进行的,这里显然高明多了,采用的定时任务,并且定时任务的执行频率也是过期时间的1/3,执行效率也比我们采用循环的方式高得多。

相关推荐
我不会起名字3224 分钟前
Redis 从 0 到 1(二十七):JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试
redis·go·序列化·protobuf·messagepack
Rain的Java大神之路10 分钟前
国家数据集团校招、社招都开了:总部雄安,这些技术岗值得认真看看
java·后端·面试
XW13 分钟前
Agent 上下文管理实现详解:从窗口工程到长期记忆
后端
我不会起名字32213 分钟前
Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字——内存构成与容量规划
redis·容量规划·内存·info·listpack
Moment14 分钟前
为什么这么多 Agent Harness 都选择 TypeScript
前端·后端·面试
我不会起名字32226 分钟前
Redis 从 0 到 1(二十五):连接池又打满了——PoolSize 公式、泄漏排查与 PoolStats
redis·连接池·go-redis·排查·poolstats
我不会起名字32228 分钟前
Redis 从 0 到 1(二十三):一个热 key 打到 10 万 QPS——hotkey 与本地缓存的 Go 实现
redis·缓存·go·qps·hotkey
软件技术NINI31 分钟前
基于SpringBoot和Vue的项目评审系统
javascript·vue.js·spring boot·后端·spring·毕业设计·毕设
无奈何杨40 分钟前
风控决策系统扩展点,自定义函数配置
后端
回家路上绕了弯1 小时前
AI Coding 中,人在做什么?
后端