Redisson 可重入分布式锁底层原理详解

一、什么是可重入锁?

可重入:同一个线程,在已经持有锁的前提下,可以再次获取同一把锁,不会出现自己把自己阻塞死的情况。

本地锁中 ReentrantLock、synchronized 都是可重入锁。 而我们手写的简易 Redis 锁,默认不支持可重入: 线程拿到锁之后,再次执行加锁代码,会直接获取锁失败,造成死锁。 所以生产环境分布式锁,一般要求支持可重入,Redisson 的 RLock 就是可重入分布式锁。

应用场景:A 方法获取锁,A 内部调用 B 方法,B 方法也需要获取同一把锁,此时就需要锁支持重入。

二、Redisson 锁在 Redis 中的存储结构

Redisson 底层不是简单的key-value字符串,而是使用hash 哈希结构存储锁信息:

XML 复制代码
key: lock:goods:1001
hash field: 线程唯一标识(uuid + threadId)
hash value: 重入计数count

示例

XML 复制代码
lock:goods:1001
  "35678190-1234-4567-8888:thread-1"  ->  1

key:锁名称

field:标记哪个线程持有这把锁

value:重入次数,每重入一次 count+1,释放一次 count-1

整个 hash 还会设置过期时间

加锁逻辑(Lua 脚本)

Redisson 加锁、重入、释放全部依靠 Lua 脚本保证原子性。

Lua 复制代码
-- 判断锁key是否不存在
if (redis.call('exists', KEYS[1]) == 0) then
    -- 不存在:新增hash,计数=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,返回nil,加锁成功
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
-- 锁存在,且不是当前线程持有,返回锁剩余过期时间,加锁失败
return redis.call('pttl', KEYS[1]);

参数说明:

  • KEYS[1]:锁 key
  • ARGV[1]:锁过期时间(毫秒)
  • ARGV[2]:当前线程唯一标识(uuid:threadId)
流程解读
  1. 如果锁不存在:创建 hash,计数置 1,设置过期时间,加锁成功
  2. 如果锁存在,并且是当前线程的锁:hincrby计数 + 1,刷新过期时间,重入成功
  3. 如果锁存在,但属于别的线程:返回锁剩余有效期,加锁失败

三、释放锁 Lua 脚本

释放锁不能直接 del,要递减计数,计数减到 0 才删除 key。

Lua 复制代码
-- 判断锁是否属于当前线程
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then
    return nil;
end;
-- 计数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then
    -- 计数>0,说明还有重入,只刷新过期时间,不删除锁
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 0;
else
    -- 计数等于0,删除锁key,释放完成
    redis.call('del', KEYS[1]);
    return 1;
end;

逻辑:

  1. 不是当前线程的锁:直接返回,禁止释放别人的锁
  2. 计数 - 1
  3. 计数 > 0:还有重入,锁不删除,更新过期时间
  4. 计数 = 0:删除锁 key,锁完全释放

四、看门狗 WatchDog 自动续期机制(重点面试题)

什么时候开启看门狗?

当你调用lock.lock()(不指定过期时间),Redisson 会自动启动看门狗;

如果手动指定 leaseTime,看门狗不会自动开启,需要自己处理锁超时风险。

默认规则:

  • 锁默认过期时间:30s
  • 看门狗定时任务:每 10s 执行一次(过期时间的 1/3)
  • 定时任务逻辑:检查锁是否还被当前线程持有,持有则刷新锁过期时间回到 30s

原理:获取锁成功后,开启一个异步定时线程,不断延长锁有效期。只要业务没执行完毕,锁就不会过期;业务执行结束,锁释放,看门狗任务停止。

⚠️注意:

  • 如果 JVM 宕机,看门狗线程直接停止,锁会等待过期自动释放,不会永久死锁。
  • 手动指定 leaseTime,Redisson 认为用户自己管控过期,不会启动看门狗,业务执行时间不能超过 leaseTime。

五、Redisson 代码演示

1. maven 依赖

XML 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.7</version>
</dependency>
  1. Redisson 配置类
java 复制代码
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}
  1. 可重入锁业务代码
java 复制代码
@Autowired
private RedissonClient redissonClient;

public void methodA(){
    RLock lock = redissonClient.getLock("lock:goods");
    try {
        // 不指定过期时间,自动开启看门狗
        lock.lock();
        System.out.println("方法A获取锁成功");
        methodB(); //内部调用方法B,再次获取同一把锁,重入生效
    }finally {
        if(lock.isHeldByCurrentThread()){
            lock.unlock();
        }
    }
}

public void methodB(){
    RLock lock = redissonClient.getLock("lock:goods");
    try {
        lock.lock();
        System.out.println("方法B重入获取锁成功");
    }finally {
        if(lock.isHeldByCurrentThread()){
            lock.unlock();
        }
    }
}

调用methodA(),A 拿到锁,调用 B,B 再次获取同一锁,计数 + 1;释放时 A、B 各 unlock 一次,计数归零,锁删除。

tryLock 非阻塞获取锁

java 复制代码
// 最多等待10s,锁持有30s,这里手动指定持有时间,看门狗失效
boolean success = lock.tryLock(10,30, TimeUnit.SECONDS);

六、Redisson 锁存在的问题:主从架构锁失效

Redis 主从同步是异步复制:

  1. 主节点执行 Lua 脚本,加锁成功,返回客户端
  2. 锁数据还没同步到从节点,主节点宕机
  3. 从节点升级为新 master,新主没有锁数据
  4. 其他线程可以再次获取锁,锁失效,并发安全被破坏

解决方案:

  1. RedLock 红锁:多独立 redis 节点,过半节点加锁成功才算获取锁。缺点:运维成本高,生产极少使用。
  2. 业务层兜底:数据库唯一索引、乐观锁、幂等,防止超卖、重复下单。

七、面试高频问答

Q1:Redisson 可重入锁底层怎么实现?

底层使用 Redis Hash 结构存储锁,field 存线程唯一标识,value 存储重入计数。加锁、释放锁全部使用 Lua 脚本保证原子操作,同一线程多次加锁计数累加,多次释放计数递减,计数归零才删除锁 key。

Q2:看门狗什么时候生效?原理是什么?

调用不带过期时间的lock.lock()时自动开启;默认 30s 过期,每 10s 刷新锁过期时间。业务执行完毕释放锁,看门狗停止。JVM 宕机,看门狗停止,锁到期自动释放。

Q3:tryLock 指定 leaseTime,看门狗还会工作吗?

不会。手动指定锁过期时间,Redisson 认为开发者自行管理锁生命周期,不会启动看门狗续期。业务执行时长不能超过 leaseTime。

Q4:为什么释放锁前要判断 isHeldByCurrentThread ()?

如果业务异常,锁已经过期自动释放,其他线程拿到锁。此时执行 unlock 会释放别人的锁,isHeldByCurrentThread()校验锁持有者,只有当前线程持有锁才允许解锁。

Q5:Redisson RLock 是公平锁吗?

默认是非公平锁;也支持公平锁getFairLock(),底层用 list 队列记录等待线程,按照先来后到获取锁,性能比非公平锁差。

八、总结

  1. Redisson 可重入锁底层基于 Redis Hash + Lua 脚本实现,支持重入计数;
  2. 看门狗自动续期,解决业务执行时间超过锁过期时间的问题;
  3. 加锁、释放全程 Lua,保证命令原子性,避免手写锁的各种坑;
  4. 主从异步复制场景下,依然存在锁失效风险,核心业务需要数据库兜底;
  5. 开发优先使用 Redisson,不要手写 Redis 分布式锁。
相关推荐
骑着蜗牛撵大象32721 小时前
558 万美元训出 671B 模型:DeepSeek-V3 算法与系统的协同拆解
分布式·算法·moe·deepseek-v3·mla·fp8训练
德迅云安全-上官21 小时前
德迅游戏盾:面向 TCP 业务的分布式云安全防护方案
分布式·tcp/ip·游戏
麻瓜code1 天前
分布式锁演进:从一个用户下两单,到 Redisson,我改了四版
分布式·wpf
skywalk81631 天前
hive —— 多机 AI Agent 协同的任务总线:分布式,多节点同时运行,保障不会宕机
人工智能·hive·分布式
一条泥憨鱼1 天前
【RabbitMQ】高级篇
java·分布式·rabbitmq·php·ruby
棣廷1 天前
初识Spark Core
大数据·分布式·spark
凯凯姜1 天前
分布式锁场景问题
分布式
每天一道题2 天前
CAP、Raft 与多数派:分布式系统如何达成一致?
分布式·后端·微服务
方方洛2 天前
ray教程-18-性能调优与反模式
人工智能·分布式·机器学习
每天一道题2 天前
分布式锁真的安全吗?从 Redis 锁、Lease 到 Fencing Token
redis·分布式·安全