目录

一、Redisson 分布式锁整体介绍

1. 核心是什么

2. 核心特性

二、加锁与解锁核心流程(Lua 原子脚本)

1. 加锁脚本(简化)

2. 解锁脚本(简化)

三、看门狗(Watchdog)机制:解决 “业务没跑完,锁先过期”

1. 看门狗作用

2. 什么时候启用 / 禁用

四、看门狗源码逐行解析(Redisson 3.x)

1. 启动看门狗:scheduleExpirationRenewal

2. 核心续期逻辑:renewExpiration

3. 真正发 Lua 续期:renewExpirationAsync

4. 解锁时停止看门狗:cancelExpirationRenewal

五、整体时序(带看门狗)

六、常见面试点总结


一、Redisson 分布式锁整体介绍

1. 核心是什么

Redisson 的分布式锁 RLock基于 Redis + Lua 原子脚本实现的可重入、阻塞、带自动续期的分布式锁,几乎对标 Java 的 ReentrantLock,但跑在分布式环境。

2. 核心特性

  • 互斥性:同一时刻只有一个客户端线程能拿到锁。
  • 可重入:同一线程可以多次 lock(),计数递增,不会自己死锁。
  • 阻塞 / 非阻塞:支持 lock()(阻塞)、tryLock()(非阻塞 / 超时)。
  • 自动过期:锁有 TTL,防止客户端宕机死锁。
  • 看门狗续期:业务没跑完就自动延长锁时间,避免误释放。
  • 释放唤醒:解锁时用 Redis Pub/Sub 通知等待线程,避免空轮询。

二、加锁与解锁核心流程(Lua 原子脚本)

1. 加锁脚本(简化)

-- KEYS[1]=lockKey, ARGV[1]=uuid:threadId, ARGV[2]=ttl
if redis.call('exists', KEYS[1]) == 0 then
    redis.call('hset', KEYS[1], ARGV[1], 1)
    redis.call('pexpire', KEYS[1], ARGV[2])
    return nil
end

-- 锁是自己的,重入
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
    redis.call('hincrby', KEYS[1], ARGV[1], 1)
    redis.call('pexpire', KEYS[1], ARGV[2])
    return nil
end

-- 锁被别人持有,返回剩余 TTL
return redis.call('pttl', KEYS[1])
  • 不存在 → 加锁 + 设置 TTL
  • 存在且是自己 → 重入计数 + 1 + 刷新 TTL
  • 存在且不是自己 → 返回剩余时间 → 抢锁失败

2. 解锁脚本(简化)

-- KEYS[1]=lockKey, KEYS[2]=channel, ARGV[1]=uuid:threadId, ARGV[2]=ttl
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
    return nil  -- 不是自己的锁,不能解
end

local cnt = redis.call('hincrby', KEYS[1], ARGV[1], -1)
if cnt > 0 then
    redis.call('pexpire', KEYS[1], ARGV[2])
    return 0
else
    redis.call('del', KEYS[1])
    redis.call('publish', KEYS[2], 'release') -- 发通知
    return 1
end
  • 必须是自己的锁才能解
  • 重入计数减 1
  • 计数 >0:只刷新 TTL,不删 key
  • 计数 =0:删 key + 发解锁通知

三、看门狗(Watchdog)机制:解决 “业务没跑完,锁先过期”

1. 看门狗作用

  • 当你调用 lock()不指定 leaseTime)时,Redisson 启动看门狗。
  • 默认:
    • lockWatchdogTimeout = 30000 ms(锁初始 TTL)
    • 10s(1/3) 自动续期一次,把锁 TTL 重置为 30s
  • 一旦 unlock() 或线程宕机 → 看门狗停止 → 锁自然过期。

2. 什么时候启用 / 禁用

  • ✅ 启用看门狗:
    lock.lock();                // 无参
    lock.lockInterruptibly();
    lock.tryLock(-1, TimeUnit.SECONDS);
    
  • ❌ 禁用看门狗(指定了 leaseTime):
    lock.lock(10, TimeUnit.SECONDS);   // 10s 后必释放
    lock.tryLock(5, TimeUnit.SECONDS);
    

四、看门狗源码逐行解析(Redisson 3.x)

入口:lock()tryAcquireAsync()scheduleExpirationRenewal()

1. 启动看门狗:scheduleExpirationRenewal

// RedissonBaseLock
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    // map: lockName -> ExpirationEntry(管理该锁的所有线程)
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);

    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        renewExpiration(); // 第一次:启动续期任务
    }
}
  • EXPIRATION_RENEWAL_MAP:全局 static map,记录 “哪个锁 + 哪些线程” 需要续期。
  • 新锁 → 调用 renewExpiration() 开始循环续期。

2. 核心续期逻辑:renewExpiration

// RedissonBaseLock
protected void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return; // 锁已释放,不用续了
    }

    // 延时任务:10s 后执行
    Timeout task = commandExecutor.getConnectionManager().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;
            }

            // 异步续期:执行 Lua 脚本,把 TTL 重置为 30s
            RFuture<Boolean> future = renewExpirationAsync(threadId);

            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock {} expiration", getRawName(), e);
                    return;
                }

                if (res) {
                    renewExpiration(); // 续期成功 → 递归:再等 10s 再续
                }
                // res=false:锁不存在了(已释放)→ 不递归,任务终止
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 10s
}

关键点:

  • newTimeout(..., 10s)10 秒后执行一次续期。
  • renewExpirationAsync(threadId):发 Lua 脚本到 Redis,续期 TTL。
  • 递归调用 renewExpiration ():只要锁还在,就每 10s 续一次,形成 “无限循环” 直到解锁。

3. 真正发 Lua 续期:renewExpirationAsync

private RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        // Lua 脚本:只有锁存在且是自己的,才续期
        "if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then " +
        "redis.call('pexpire', KEYS[1], ARGV[2]); " +
        "return true; " +
        "end; " +
        "return false;",
        Collections.singletonList(getRawName()),
        getLockName(threadId), internalLockLeaseTime); // 30s
}

逻辑:

  • 检查 hash 里有自己的 threadId 吗?
  • 有 → pexpire 重置为 30s → 返回 true → 继续续期
  • 没有 → 返回 false → 看门狗停止

4. 解锁时停止看门狗:cancelExpirationRenewal

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

    if (threadId != null) {
        entry.removeThreadId(threadId);
    }

    // 没有线程再持有这把锁 → 从 map 移除 → 下次 renewExpiration 直接 return
    if (entry.isEmpty()) {
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}
  • unlock() 时调用
  • 把当前线程从 ExpirationEntry 移除
  • 空了就删 key → 看门狗下次执行发现 ee == null → 终止循环

五、整体时序(带看门狗)

  1. lock() → Redis:不存在 → set + expire 30s
  2. 启动看门狗:10s 后执行
  3. 10s 到 → Lua:锁存在且是自己 → expire 30s → 再等 10s
  4. 业务跑完 → unlock() → 删 key + 停看门狗
  5. 如果宕机 → 看门狗线程死 → 不再续期 → 30s 后 Redis 自动删 key

六、常见面试点总结

  • 看门狗什么时候开? 无参 lock(),不指定 leaseTime
  • 默认参数? 30s TTL,每 10s 续期。
  • 续期原理? 定时任务 + Lua 原子检查 + 递归循环。
  • 怎么停? unlock() 主动停;宕机被动停。
  • 和普通 Redis 锁区别? 自动续期、可重入、Pub/Sub 唤醒、防死锁更完善。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐