一、什么是可重入锁?
可重入:同一个线程,在已经持有锁的前提下,可以再次获取同一把锁,不会出现自己把自己阻塞死的情况。
本地锁中ReentrantLock、synchronized都是可重入锁。 而我们手写的简易 Redis 锁,默认不支持可重入: 线程拿到锁之后,再次执行加锁代码,会直接获取锁失败,造成死锁。 所以生产环境分布式锁,一般要求支持可重入,Redisson 的 RLock 就是可重入分布式锁。
应用场景:A 方法获取锁,A 内部调用 B 方法,B 方法也需要获取同一把锁,此时就需要锁支持重入。
二、Redisson 锁在 Redis 中的存储结构
Redisson 底层不是简单的key-value字符串,而是使用hash 哈希结构存储锁信息:
key: lock:goods:1001 hash field: 线程唯一标识(uuid + threadId) hash value: 重入计数count示例
lock:goods:1001 "35678190-1234-4567-8888:thread-1" -> 1key:锁名称
field:标记哪个线程持有这把锁
value:重入次数,每重入一次 count+1,释放一次 count-1
整个 hash 还会设置过期时间
加锁逻辑(Lua 脚本)
Redisson 加锁、重入、释放全部依靠 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]:锁 keyARGV[1]:锁过期时间(毫秒)ARGV[2]:当前线程唯一标识(uuid:threadId)
流程解读
- 如果锁不存在:创建 hash,计数置 1,设置过期时间,加锁成功
- 如果锁存在,并且是当前线程的锁:
hincrby计数 + 1,刷新过期时间,重入成功 - 如果锁存在,但属于别的线程:返回锁剩余有效期,加锁失败
三、释放锁 Lua 脚本
释放锁不能直接 del,要递减计数,计数减到 0 才删除 key。
-- 判断锁是否属于当前线程 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
- 计数 > 0:还有重入,锁不删除,更新过期时间
- 计数 = 0:删除锁 key,锁完全释放
四、看门狗 WatchDog 自动续期机制(重点面试题)
什么时候开启看门狗?
当你调用lock.lock()(不指定过期时间),Redisson 会自动启动看门狗;
如果手动指定 leaseTime,看门狗不会自动开启,需要自己处理锁超时风险。
默认规则:
- 锁默认过期时间:30s
- 看门狗定时任务:每 10s 执行一次(过期时间的 1/3)
- 定时任务逻辑:检查锁是否还被当前线程持有,持有则刷新锁过期时间回到 30s
原理:获取锁成功后,开启一个异步定时线程,不断延长锁有效期。只要业务没执行完毕,锁就不会过期;业务执行结束,锁释放,看门狗任务停止。
⚠️注意:
- 如果 JVM 宕机,看门狗线程直接停止,锁会等待过期自动释放,不会永久死锁。
- 手动指定 leaseTime,Redisson 认为用户自己管控过期,不会启动看门狗,业务执行时间不能超过 leaseTime。
五、Redisson 代码演示
1. maven 依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.17.7</version> </dependency>2. Redisson 配置类
@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); } }3. 可重入锁业务代码
@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 非阻塞获取锁
// 最多等待10s,锁持有30s,这里手动指定持有时间,看门狗失效 boolean success = lock.tryLock(10,30, TimeUnit.SECONDS);六、Redisson 锁存在的问题:主从架构锁失效
Redis 主从同步是异步复制:
- 主节点执行 Lua 脚本,加锁成功,返回客户端
- 锁数据还没同步到从节点,主节点宕机
- 从节点升级为新 master,新主没有锁数据
- 其他线程可以再次获取锁,锁失效,并发安全被破坏
解决方案:
- RedLock 红锁:多独立 redis 节点,过半节点加锁成功才算获取锁。缺点:运维成本高,生产极少使用。
- 业务层兜底:数据库唯一索引、乐观锁、幂等,防止超卖、重复下单。
七、面试高频问答
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 队列记录等待线程,按照先来后到获取锁,性能比非公平锁差。
八、总结
- Redisson 可重入锁底层基于 Redis Hash + Lua 脚本实现,支持重入计数;
- 看门狗自动续期,解决业务执行时间超过锁过期时间的问题;
- 加锁、释放全程 Lua,保证命令原子性,避免手写锁的各种坑;
- 主从异步复制场景下,依然存在锁失效风险,核心业务需要数据库兜底;
- 开发优先使用 Redisson,不要手写 Redis 分布式锁。