Redisson 分布式锁复习总结

一、 为什么需要 Redisson?(基于 setnx 的问题)

手动实现的 setnx 锁存在四大痛点,Redisson 逐一解决:

痛点 描述 Redisson 解决方案
不可重入 同一个线程无法多次获取同一把锁,导致嵌套调用死锁。 Hash 结构存储计数器,支持同一线程多次加锁。
不可重试 获取锁失败直接返回,无重试机制。 tryLock 方法支持等待时间,内部通过订阅与自旋实现重试。
超时释放 业务执行时间 > 锁过期时间,导致锁自动释放,安全隐患。 WatchDog(看门狗)机制,自动续期,默认每 10s 续约至 30s。
主从一致性 主节点写入锁后宕机,未同步到从节点,导致锁丢失。 MultiLock(联锁),将多个 Redis 节点视为独立节点,须全部加锁成功。

二、 快速入门与基本使用

1. 依赖与配置

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
        return Redisson.create(config);
    }
}

2. 核心代码模板

RLock lock = redissonClient.getLock("lock:order:" + userId);
// 尝试获取锁
// boolean isLock = lock.tryLock(); // 无参:立即返回
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); // 有参:等待1s,锁自动释放10s(WatchDog失效)
if (isLock) {
    try {
        // 执行业务逻辑
    } finally {
        lock.unlock(); // 释放锁
    }
}

在这里插入图片描述

三、 核心原理:可重入锁

1. 实现原理

Redisson 放弃了 setnx 的简单 String 结构,改用 Redis Hash 结构

  • Key:锁名称(如 lock:order:1
  • Field:线程标识(UUID + ThreadID
  • Value:重入次数(计数器)

2. Lua 脚本逻辑 (加锁)

Redisson 使用 Lua 脚本保证原子性:

  1. 锁不存在:创建 Hash,hset 字段值为 1,设置过期时间。返回 null(成功)。
  2. 锁存在且是自己hincrby 将计数器 +1,重置过期时间。返回 null(成功)。
  3. 锁存在但不是自己:返回锁的剩余生存时间(失败)。

3. 解锁逻辑

  • 将计数器 -1
  • 若计数器 > 0:说明还有重入,保留锁,重置过期时间。
  • 若计数器 <= 0:删除 Key,发布解锁消息。

在这里插入图片描述

分布式锁原理

🧩 Redisson 分布式锁核心流程拆解

我帮你把这张流程图和对应的源码逻辑一步步拆开讲,你就能看懂每个判断和分支的意义了。


在这里插入图片描述

1. 开始:尝试获取锁

入口是 RLock.lock()RLock.tryLock() 方法,本质是向 Redis 发送 SET 命令尝试创建锁键(比如 redisson_lock:key)。

2. 判断 ttl 是否为 null
  • ttl = null:代表获取锁成功(锁键不存在,成功写入)
  • ttl ≠ null:代表获取锁失败(锁已被其他线程持有,返回当前锁的剩余过期时间)

对应源码:tryLockInnerAsync() 方法,执行 SET 命令后,返回 null 表示成功,返回 ttl 表示失败。


3. 分支1:获取锁成功(ttl = null)
  • 判断 leaseTime 是否为 -1
    • 是(leaseTime = -1):开启 watchDog(看门狗) 自动续期机制
      Redisson 会启动一个后台定时任务,每隔 lockWatchdogTimeout / 3 时间(默认 10s/3≈3.3s),给锁键重置过期时间(默认 30s),防止业务执行时间过长导致锁提前过期。leaseTime=-1 = 永久自动续期,直到解锁或服务挂掉。
      不管业务是运行、阻塞、休眠,都续期。(超时续约)

    • 否(leaseTime > 0):不开启看门狗,锁会在 leaseTime 后自动过期

  • 最终返回 true,表示加锁成功

对应源码:lock() 方法默认 leaseTime = -1,会自动续期;tryLock(waitTime, leaseTime, unit) 手动指定 leaseTime 则不会续期。


4. 分支2:获取锁失败(ttl ≠ null)

进入等待重试逻辑:

  1. 判断剩余等待时间是否 > 0
    • :直接返回 false,表示加锁失败
    • :订阅 Redis 的 pub/sub 通道,等待锁释放的信号
  2. 订阅后,阻塞等待锁释放通知
    • 等待过程中会不断检查等待时间是否超时
      • 超时:取消订阅,返回 false
      • 未超时:收到锁释放信号后,回到「尝试获取锁」步骤,重新竞争锁

对应源码:subscribe() 方法订阅锁释放频道,await() 阻塞等待信号,超时后通过 cancel() 取消订阅并返回失败。


💡 关键源码概念对应

流程图节点 源码对应 核心作用
ttl 是否为 null tryLockInnerAsync() 返回值 标识锁是否获取成功
leaseTime 是否为 -1 lock() 方法参数 控制是否开启自动续期
开启 watchDog RenewExpirationTask 定时任务 后台续期,防止锁提前过期
订阅并等待释放锁的信号 LockPubSub 发布订阅 避免自旋浪费 CPU,高效等待锁
判断等待时间是否超时 await(timeout, unit) 控制最大等待时间,防止无限阻塞

✅ 完整流程总结

  1. 尝试加锁 → 成功则根据 leaseTime 决定是否续期 → 返回 true
  2. 加锁失败 → 若还有等待时间 → 订阅锁释放信号 → 等待唤醒后重试
  3. 等待超时或无剩余等待时间 → 返回 false,加锁失败

❓ 你可能看不懂的点补充

  • watchDog 为什么只在 leaseTime=-1 时开启?
    手动指定 leaseTime 代表你能预估业务执行时间,Redisson 认为不需要自动续期,避免锁长期占用。
  • 订阅信号比自旋好在哪?
    自旋会不断发送 GET/SET 命令,浪费 CPU 和 Redis 资源;订阅模式只有锁释放时才唤醒,更高效。
  • ttl 是什么?
    time to live,Redis 键的剩余过期时间,Redisson 用它判断锁是否还在被持有。
    在这里插入图片描述

🔓 Redisson 释放锁流程完整解析

把右边释放锁的流程和左边加锁的联动逻辑讲透,你就能看懂整个闭环了。


1. 入口:尝试释放锁

调用 RLock.unlock() 方法,本质是执行 Redis Lua 脚本:

  • 校验当前线程是否持有该锁(防止误删别人的锁)
  • 删除锁键
  • 触发后续清理逻辑

2. 判断是否成功
  • :释放锁成功(Lua 脚本执行完成,锁已删除)
  • :释放锁失败(比如锁已过期、不是自己持有的锁),记录异常后结束

对应源码:unlockInnerAsync() 方法,通过 Lua 脚本原子性完成「校验+删除」。


3. 释放成功后的核心动作

释放锁成功后,会触发两个关键操作:

① 取消 watchDog(看门狗)
  • 停止后台自动续期的定时任务
  • 避免锁已经释放了,还在白白刷新过期时间
  • 这一步和左边加锁流程里的「开启 watchDog」是生命周期配对
② 发送释放锁的消息(Pub/Sub)
  • 向 Redis 的 lock 频道发布「锁已释放」的通知
  • 唤醒所有正在订阅这个锁的等待线程(左边流程里「订阅并等待释放锁的信号」的线程)
  • 等待线程收到通知后,会重新进入「尝试获取锁」的竞争环节

4. 与加锁流程的联动

这张图里的绿色虚线就是关键联动:

  • 释放锁成功 → 取消当前持锁线程的 watchDog
  • 释放锁成功 → 发送通知,唤醒正在等待的线程,让它们重新尝试获取锁
  • 等待线程被唤醒后,回到「尝试获取锁」的起点,继续竞争

💡 关键源码概念对应

流程图节点 源码对应 核心作用
尝试释放锁 unlock() / unlockInnerAsync() 执行 Lua 脚本释放锁
判断是否成功 Lua 脚本返回值 校验锁归属与删除结果
取消 watchDog cancelExpirationRenewal() 停止自动续期任务
发送释放锁消息 publishUnlock() 发布 Pub/Sub 通知唤醒等待者
记录异常 日志/抛出 IllegalMonitorStateException 处理非法释放锁的场景

✅ 完整释放锁流程总结

  1. 调用 unlock() → 执行 Lua 脚本校验并删除锁
  2. 释放成功:
    • 停止看门狗续期任务
    • 发布锁释放通知,唤醒等待线程
    • 结束
  3. 释放失败:
    • 记录异常(比如「锁不属于当前线程」)
    • 结束

❓ 你可能关心的细节补充

  • 为什么用 Lua 脚本?
    保证「判断锁归属 + 删除锁」是原子操作,避免并发问题。
  • 为什么要发 Pub/Sub 消息?
    替代自旋等待,让等待线程在锁释放时才被唤醒,更高效。
  • 如果服务宕机了,锁会怎样?
    看门狗随服务一起停止,锁会在 lockWatchdogTimeout(默认30秒)后自动过期,避免死锁。

四、 进阶原理:锁重试与 WatchDog

1. WatchDog (看门狗机制)

目的:解决业务执行时间过长导致锁超时释放的问题。

  • 触发条件:调用 lock()tryLock() 不带 LeaseTime 参数时。
  • 默认配置:锁过期时间 30s,检查间隔 10s
  • 工作流程
    1. 加锁成功后,启动一个后台定时任务。
    2. 每隔 10s(过期时间/3)检查锁是否还被当前线程持有。
    3. 若持有,重置过期时间为 30s(续约)。
    4. 若线程宕机,看门狗停止,锁等待 30s 后自动释放。

2. 锁重试机制

  • tryLock(waitTime, leaseTime, unit)
    • 如果设置了 leaseTime,看门狗机制失效,锁会在指定时间后释放。
    • 获取失败时,会利用 Redis 的 Pub/Sub(发布订阅) 机制等待锁释放通知,而不是纯粹的死循环自旋(性能更优)。

五、 高可用原理:MultiLock (联锁)

1. 问题背景

在 Redis 主从架构中:

  1. 客户端向 Master 写入锁。
  2. Master 宕机,数据未同步到 Slave。
  3. Slave 升级为新 Master,锁数据丢失。
  4. 其他客户端成功获取锁,导致并发安全问题

2. MultiLock 解决方案

原理:放弃主从一致性,将多个 Redis 节点视为独立的 Master。

  • 加锁时:必须向所有节点都发送加锁请求,全部成功才算加锁成功。
  • 容错性:只要有任意一个节点存活,锁就不会丢失(类似 CP 模型)。
    代价:性能下降(需要连接多个节点并发写入)。

六、 复习口诀

  1. 重入:Hash 结构存 ID,计数加减保原子。
  2. 续约:看门狗,定时跑,十秒一刷三十秒。
  3. 重试:消息订阅等通知,避免空转耗资源。
  4. 高可用:主从同步怕宕机,联锁多写保安全。
Logo

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

更多推荐