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 分布式锁。
相关推荐
此时不提桶,更待何时18 小时前
06-12-A-Kafka存储深水区与源码解析详解
分布式·kafka·linq
此时不提桶,更待何时21 小时前
06-15-A-Kafka生态集成与流处理详解
分布式·kafka
此时不提桶,更待何时1 天前
06-13-A-Kafka客户端与协议深入详解
分布式·kafka
程序猿乐锅1 天前
从0-1一文详解RabbitMQ
java·分布式·后端·中间件·rabbitmq·ruby
老陈说编程2 天前
1. 鸿蒙 (HarmonyOS) 2012 至 2026 年的发展历程
分布式·华为·个人开发·harmonyos·鸿蒙·鸿蒙系统·程序员创富
ly76894 天前
Redis 分布式锁的边界条件:Redlock 争议、锁续期与客户端崩溃后的互斥失效
数据库·redis·分布式·分布式锁·watchdog·redlock
灯澜忆梦4 天前
【minio】#5 | MinIO 分布式部署 + HTTPS 部署
分布式·网络协议·https·对象存储·minio
谢亮_vipxieliang4 天前
Spring Cloud 服务治理入门:注册发现、配置中心、网关限流、熔断降级与分布式一致性方案
分布式·spring·spring cloud