一、什么是可重入锁?
可重入:同一个线程,在已经持有锁的前提下,可以再次获取同一把锁,不会出现自己把自己阻塞死的情况。
本地锁中 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]:锁 keyARGV[1]:锁过期时间(毫秒)ARGV[2]:当前线程唯一标识(uuid:threadId)
流程解读
- 如果锁不存在:创建 hash,计数置 1,设置过期时间,加锁成功
- 如果锁存在,并且是当前线程的锁:
hincrby计数 + 1,刷新过期时间,重入成功 - 如果锁存在,但属于别的线程:返回锁剩余有效期,加锁失败
三、释放锁 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
- 计数 > 0:还有重入,锁不删除,更新过期时间
- 计数 = 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>
- 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);
}
}
- 可重入锁业务代码
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 主从同步是异步复制:
- 主节点执行 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 分布式锁。