侧边栏壁纸
  • 累计撰写 2 篇文章
  • 累计创建 5 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

Redisson 分布式锁原理详解

Evans
2026-11-08 / 0 评论 / 0 点赞 / 6 阅读 / 3,368 字

本章学习重点:加锁、解锁、可重入、Watch Dog 自动续期、Lua 原子性、Fencing Token。

一、Redisson 分布式锁概述

Redisson 是基于 Redis 实现的 Java 分布式基础组件,提供了 RLock 分布式锁。

与简单的 Redis SETNX 实现不同,Redisson 提供了:

  • 可重入锁
  • 自动过期机制
  • Watch Dog 自动续期
  • Pub/Sub 锁释放通知
  • Lua 脚本保证操作原子性
  • 公平锁、读写锁等扩展能力

基本使用:

RLock lock = redissonClient.getLock("order_lock");

try {
    lock.lock();

    // 执行业务逻辑
    createOrder();

} finally {
    lock.unlock();
}

二、加锁原理

2.1 Redis 数据结构

Redisson 默认使用 Redis Hash 保存锁信息。

假设 JVM-1 中的线程 A 获取了 order_lock。

Redis 中的数据可以理解为:

Key: order_lock
Type: Hash

Field: 客户端ID:线程ID
Value: 1

其中:

  • 客户端 ID:区分不同 Redisson 客户端。
  • 线程 ID:区分同一个 JVM 中的不同线程。
  • Value:记录锁的重入次数。

为什么需要客户端 ID 和线程 ID?

因为分布式环境中,不仅要区分不同 JVM,还要区分同一个 JVM 内的不同线程。

2.2 加锁核心逻辑

Redisson 通过 Lua 脚本保证加锁过程的原子性。

简化后的 Lua 脚本:

-- 锁不存在
if (redis.call('exists', KEYS[1]) == 0) then

    -- 创建锁,记录持有者,重入次数为 1
    redis.call('hincrby', KEYS[1], ARGV[2], 1)

    -- 设置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1])

    return nil
end

-- 锁已经存在,但持有者是当前线程
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then

    -- 重入次数 +1
    redis.call('hincrby', KEYS[1], ARGV[2], 1)

    -- 刷新过期时间
    redis.call('pexpire', KEYS[1], ARGV[1])

    return nil
end

-- 锁被其他线程持有,返回剩余过期时间
return redis.call('pttl', KEYS[1])

说明:

这是用于理解的简化版本,不是 Redisson 完整源码。

2.3 三种加锁情况

情况一:锁不存在

执行:

HINCRBY order_lock 客户端ID:线程ID 1
PEXPIRE order_lock 30000

当前线程成功获取锁。

情况二:锁已经存在,但持有者是当前线程

说明当前线程再次获取锁。

执行:

HINCRBY order_lock 客户端ID:线程ID 1

重入次数增加,实现可重入。

情况三:锁被其他线程持有

当前线程无法获取锁,返回锁剩余 TTL。

Redisson 可以通过 Pub/Sub 订阅锁释放通知,等待重新竞争,而不是一直无脑轮询 Redis。

2.4 为什么使用 Lua?

假设不使用 Lua,而是通过 Java 分别执行判断和创建:

if (!redis.exists(key)) {
    redis.hset(key, field, 1);
}

可能出现:

  1. 线程 A 判断 Key 不存在。
  2. 线程 B 也判断 Key 不存在。
  3. A 写入锁。
  4. B 也写入锁。

最终两个线程都认为自己获取了锁。

Lua 脚本在 Redis 服务端原子执行,中间不会被其他客户端命令插入。

因此可以保证判断、创建、设置过期时间这一系列操作的原子性。


三、可重入锁原理

Redisson 的 RLock 支持可重入。

例如:

lock.lock();
lock.lock();
lock.lock();

同一个线程可以重复获取同一把锁。

Redis 中的重入次数变化:

操作 重入次数
第一次 lock() 1
第二次 lock() 2
第三次 lock() 3

可重入的核心是:

通过客户端 ID + 线程 ID 判断是否为当前锁持有者。

如果是当前线程再次获取锁,则重入次数加一,而不是阻塞自己。

这与 Java ReentrantLock 的设计思想类似。


四、解锁原理

4.1 为什么只有持有者才能解锁?

假设:

  • JVM-1 的线程 A 获取锁。
  • JVM-2 的线程 B 尝试获取锁,但失败。
  • 线程 B 不能直接删除 Redis 中的锁。

否则可能出现线程 A 还在执行业务,线程 B 却提前释放锁的问题。

因此 Redisson 解锁时会校验当前线程是否为锁持有者。

4.2 解锁核心 Lua 脚本

简化后的逻辑:

-- 判断当前线程是否持有锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil
end

-- 重入次数 -1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1)

-- 重入次数仍然大于 0
if (counter > 0) then

    redis.call('pexpire', KEYS[1], ARGV[2])

    return 0
else

    -- 重入次数归零,删除锁
    redis.call('del', KEYS[1])

    -- 发布锁释放通知
    redis.call('publish', KEYS[2], ARGV[1])

    return 1
end

这是帮助理解的简化版本。

4.3 解锁过程

例如:

lock.lock(); // 1
lock.lock(); // 2
lock.lock(); // 3

lock.unlock(); // 2
lock.unlock(); // 1
lock.unlock(); // 0

只有重入次数归零时,才真正删除 Redis 中的锁。

4.4 Pub/Sub 通知机制

当锁真正释放时,Redisson 会通过 Redis Pub/Sub 发布解锁通知。

假设:

线程 A:持有锁
线程 B:等待锁
线程 C:等待锁

线程 A 解锁:

线程 A
  |
  | unlock()
  v
Redis 删除锁
  |
  | PUBLISH
  v
Redis 发布解锁通知
  |
  +------> 线程 B 收到通知
  |
  +------> 线程 C 收到通知

等待线程收到通知后重新尝试获取锁。

注意:

收到通知不代表一定能拿到锁。

线程 B 和线程 C 仍然需要竞争,最终只有一个线程能够成功获取锁。


五、Watch Dog 自动续期机制

这是 Redisson 最重要的特性之一,也是面试高频考点。

5.1 为什么需要自动续期?

假设:

lock.lock(30, TimeUnit.SECONDS);

业务执行需要 60 秒。

如果锁的有效期只有 30 秒:

0 秒:线程 A 获取锁
 |
 v
30 秒:锁过期
 |
 v
线程 B 获取锁
 |
 v
60 秒:线程 A 仍然在执行

这时候线程 A 和线程 B 可能同时执行临界区代码,造成并发安全问题。

为了解决这个问题,Redisson 提供了 Watch Dog 机制。

5.2 Watch Dog 是什么?

Watch Dog(看门狗)是一种自动续期机制。

当线程成功获取锁后,如果没有指定 leaseTime,Redisson 会启动定时续期任务。

定期检查锁是否仍然属于当前线程:

  • 如果仍然持有锁,则延长过期时间。
  • 如果已经不持有锁,则停止续期。

默认配置:

Config config = new Config();

// 默认 30 秒
config.setLockWatchdogTimeout(30000);

5.3 自动续期时间

默认锁过期时间为 30 秒。

续期周期通常为过期时间的 1/3,即 10 秒。

0 秒:获取锁,TTL = 30 秒
 |
 v
10 秒:第一次续期,TTL = 30 秒
 |
 v
20 秒:第二次续期,TTL = 30 秒
 |
 v
30 秒:第三次续期,TTL = 30 秒
 |
 v
持续续期,直到主动释放锁

注意:

每次续期都是重新设置 30 秒,而不是在原有剩余时间上增加 30 秒。

5.4 Watch Dog 续期原理

Redisson 内部通过定时任务和 Lua 脚本实现续期。

简化后的 Java 逻辑:

private void renewExpiration() {

    // 定时执行续期任务
    scheduler.schedule(() -> {

        // 执行 Lua 脚本续期
        renewExpirationAsync();

    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}

续期 Lua 脚本:

-- 判断当前线程是否仍然持有锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then

    -- 重新设置过期时间
    return redis.call('pexpire', KEYS[1], ARGV[1])

end

return 0

核心思想:

续期之前必须验证锁的持有者,不能无条件延长锁的过期时间。

续期成功后,Redisson 会继续安排下一次续期任务。

如果发现锁已经不属于当前线程,或者锁不存在,就停止续期。


六、lock() 与 lock(leaseTime) 的区别

这是非常容易混淆的知识点。

核心结论:是否指定 leaseTime,决定是否启用 Watch Dog。

6.1 两种方式

方式一:不指定租约时间。

lock.lock();

特点:

  • 使用默认锁过期时间。
  • 默认 30 秒。
  • 启动 Watch Dog。
  • 业务执行时间较长时可以自动续期。
  • 主动 unlock 后释放锁并停止续期。

方式二:指定租约时间。

lock.lock(30, TimeUnit.SECONDS);

特点:

  • 指定锁的租约时间为 30 秒。
  • 不启动 Watch Dog。
  • 30 秒后锁自动过期。
  • 不会因为业务未完成而自动续期。

6.2 tryLock 的不同重载

// 立即尝试获取锁
lock.tryLock();

// 最多等待 10 秒,没有指定租约时间
lock.tryLock(10, TimeUnit.SECONDS);

// 最多等待 10 秒,获取后租约时间为 30 秒
lock.tryLock(10, 30, TimeUnit.SECONDS);

方法对比:

方法 等待获取锁 锁租约时间 Watch Dog
lock() 无限等待 默认 30 秒 启用
lock(30, SECONDS) 无限等待 30 秒 不启用
tryLock() 立即尝试 默认 30 秒 启用
tryLock(10, SECONDS) 最多等待 10 秒 默认 30 秒 启用
tryLock(10, 30, SECONDS) 最多等待 10 秒 30 秒 不启用

特别注意:

waitTime 和 leaseTime 是两个完全不同的概念。

  • waitTime:等待获取锁的最长时间。
  • leaseTime:成功获取锁之后,锁的租约时间。

6.3 如何选择?

如果业务执行时间不确定:

lock.lock();

让 Watch Dog 自动续期。

如果业务执行时间明确,并且希望锁到期自动释放:

lock.lock(30, TimeUnit.SECONDS);

指定租约时间。

实际项目中需要根据业务特点选择,不能盲目认为指定过期时间一定更安全。


七、JVM 宕机与锁失效风险

7.1 JVM 宕机

假设:

  1. JVM-1 获取锁。
  2. Watch Dog 定期续期。
  3. JVM-1 突然宕机。
  4. 续期任务停止。
  5. Redis 中的锁最终 TTL 到期。
  6. 其他线程可以重新获取锁。

因此 Watch Dog 也解决了 JVM 宕机后锁无法释放的问题。

7.2 STW 导致锁过期

还有一种特殊情况:

  1. JVM-1 获取锁。
  2. JVM-1 发生长时间 STW。
  3. Watch Dog 无法及时执行续期。
  4. Redis 中的锁过期。
  5. JVM-2 获取锁。
  6. JVM-1 恢复运行。

这时候 JVM-1 原来的线程可能继续执行已经失去锁保护的业务代码。

因此:

Watch Dog 不意味着锁绝对不会失效。

对于要求严格的分布式业务,还需要考虑 Fencing Token。


八、Fencing Token 栅栏令牌

8.1 为什么需要 Fencing Token?

分布式锁只能控制锁的获取,不能天然保证锁过期后的旧持有者无法继续操作业务资源。

例如:

线程 获取锁 Token
A 第一次获取 1
B A 的锁过期后获取 2
A STW 恢复继续执行 1

此时 A 虽然已经失去锁,但仍可能继续写入业务数据。

8.2 Fencing Token 如何解决?

每次获取锁时生成一个递增的令牌。

业务数据库或资源服务记录当前已经接受的最大 Token。

例如:

当前最大 Token = 2

线程 A 携带 Token = 1
数据库拒绝写入

线程 B 携带 Token = 2
数据库允许写入

这样就可以避免旧持有者继续修改业务资源。

Redisson 提供了 RFencedLock 支持 Fencing Token。

但需要注意:

资源服务必须主动校验 Token,否则令牌本身无法保护业务数据。


九、Redisson 分布式锁完整生命周期

          lock()
            |
            v
      执行 Lua 加锁
            |
            v
       获取锁成功
            |
            v
      记录持有者信息
            |
            v
      启动 Watch Dog
            |
            v
        执行业务
            |
            v
      定时检查锁持有者
            |
            v
        自动续期
            |
            v
         unlock()
            |
            v
       重入次数 -1
            |
            v
      判断是否归零
            |
       +----+----+
       |         |
      否         是
       |         |
       v         v
    保留锁     删除 Redis 锁
                 |
                 v
             发布通知
                 |
                 v
             停止续期
                 |
                 v
          等待线程重新竞争

十、核心知识点速记

加锁

  • Redis Hash 保存锁信息。
  • 客户端 ID + 线程 ID 标识持有者。
  • 重入次数记录在 Hash 中。
  • Lua 保证加锁操作原子性。

解锁

  • 校验当前线程是否持有锁。
  • 重入次数减一。
  • 重入次数归零才删除锁。
  • 删除锁后通过 Pub/Sub 发布通知。

自动续期

  • 不指定 leaseTime 才启动 Watch Dog。
  • 默认锁过期时间 30 秒。
  • 通常每 10 秒执行一次续期。
  • 续期前校验当前线程是否仍持有锁。
  • 主动解锁后停止续期。

风险控制

  • JVM 宕机:依靠 TTL 自动释放锁。
  • STW:可能导致续期不及时。
  • 锁过期:旧线程可能继续执行。
  • Fencing Token:防止旧持有者写入过期数据。

最终记住一句话:

Redisson 通过 Redis Hash 实现可重入锁,通过 Lua 保证加锁和解锁的原子性,通过 Pub/Sub 通知等待线程,通过 Watch Dog 实现自动续期。

其中,是否指定 leaseTime,是决定是否启用 Watch Dog 的关键。

0

评论区