面试官:你用过Redisson吗?看门狗机制知道吗?
候选人:知道!就是自动续期...
面试官:底层怎么实现的?Lua脚本能说说吗?
候选人:😰💦(这...)
别慌!今天我们深入Redisson源码,把看门狗机制讲得明明白白!
🎬 开篇:为什么需要Redisson?
原始Redis锁的痛点
// 痛点1:需要手动设置过期时间
redisTemplate.setIfAbsent("lock", "value", 10, TimeUnit.SECONDS);
// 痛点2:如果业务执行超过10秒怎么办?
// - 设置太短:业务还没完成,锁就释放了
// - 设置太长:出异常后,锁长时间无法释放
// 痛点3:需要自己写Lua脚本释放锁
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1]...";
// 痛点4:无法实现可重入
// 痛点5:无法实现读写锁
// 痛点6:无法实现公平锁
Redisson的解决方案
// 一行代码搞定!
RLock lock = redisson.getLock("myLock");
lock.lock(); // 自动续期,自动释放,支持可重入!
try {
// 业务逻辑,不用担心执行时间
Thread.sleep(100000); // 即使100秒也没问题!
} finally {
lock.unlock();
}
🔍 第一章:Redisson架构总览
核心组件
RedissonClient
│
├─ RLock (普通锁)
├─ RReadWriteLock (读写锁)
├─ RSemaphore (信号量)
├─ RCountDownLatch (倒计数锁)
└─ RPermitExpirableSemaphore (可过期信号量)
底层依赖:
├─ Netty (网络通信)
├─ Lua脚本 (原子操作)
└─ 看门狗线程 (自动续期)
🎭 生活比喻:智能停车系统
传统停车场(原始Redis锁):
1. 你交10元,可以停2小时
2. 如果2小时没出来,车被锁 😰
3. 需要自己计算时间,提前续费
Redisson停车场(智能系统):
1. 你进入停车场,系统自动记录
2. 系统检测到你还在,自动续费 🎉
3. 你离开时,系统自动结算
4. 同一辆车可以多次进出(可重入)
🐕 第二章:看门狗机制深度剖析
核心原理
看门狗 = 定时任务 + 自动续期
默认参数:
- lockWatchdogTimeout = 30秒 (锁过期时间)
- 续期间隔 = 10秒 (lockWatchdogTimeout / 3)
时间轴:
T0: 获取锁,设置30秒过期
T10: 看门狗检查:业务还在执行 → 续期到T40
T20: 看门狗检查:业务还在执行 → 续期到T50
T25: 业务完成,手动unlock → 停止看门狗
💻 源码分析(简化版)
第一步:加锁时启动看门狗
// RedissonLock.java
public void lock() {
try {
// 尝试加锁,-1表示不设置等待时间
lockInterruptibly(-1, null);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void lockInterruptibly(long leaseTime, TimeUnit unit) {
// 1. 获取当前线程ID
long threadId = Thread.currentThread().getId();
// 2. 尝试获取锁
Long ttl = tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
// 加锁成功!
return;
}
// 3. 加锁失败,订阅锁释放事件,等待...
}
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
// 如果手动指定了过期时间,直接使用,不启动看门狗
return tryLockInner(leaseTime, unit, threadId);
}
// 4. 没有指定过期时间,使用默认的30秒,并启动看门狗!
return tryAcquireAsync(
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS,
threadId
).get();
}
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (entry != null) {
return; // 已经启动过了
}
// 创建一个定时任务
ExpirationEntry newEntry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(
getEntryName(), newEntry
);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
newEntry.addThreadId(threadId);
// 🔥 核心:启动续期任务
renewExpiration();
}
}
第二步:看门狗续期逻辑
// RedissonLock.java
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return; // 锁已经释放了
}
// 创建一个定时任务,延迟 internalLockLeaseTime/3 后执行
// internalLockLeaseTime默认30秒,所以是10秒后执行
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return; // 锁已经释放
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 🔥 核心:执行续期操作(Lua脚本)
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("续期失败", e);
return;
}
if (res) {
// 续期成功,递归调用自己,继续下一次续期
renewExpiration();
} else {
// 续期失败(锁已经不存在),停止续期
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
// 续期的Lua脚本
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE,
RedisCommands.EVAL_BOOLEAN,
// Lua脚本:如果锁存在,就续期到30秒
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId)
);
}
第三步:释放锁时停止看门狗
// RedissonLock.java
public void unlock() {
// 1. 执行释放锁的Lua脚本
Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
if (opStatus == null) {
throw new IllegalMonitorStateException(
"尝试释放锁,但锁不属于当前线程"
);
}
// 2. 🔥 停止看门狗
cancelExpirationRenewal(Thread.currentThread().getId());
}
void cancelExpirationRenewal(Long threadId) {
ExpirationEntry entry = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (entry == null) {
return;
}
if (threadId != null) {
entry.removeThreadId(threadId);
}
if (threadId == null || entry.hasNoThreads()) {
// 取消定时任务
Timeout timeout = entry.getTimeout();
if (timeout != null) {
timeout.cancel(); // 停止看门狗
}
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
🔥 核心Lua脚本详解
加锁脚本
-- KEYS[1]: 锁的key (例如: "myLock")
-- ARGV[1]: 过期时间 (30000毫秒)
-- ARGV[2]: 唯一标识 (UUID:threadId)
-- 检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
-- 锁不存在,创建锁(Hash结构)
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
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 刷新过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil; -- 重入成功
end;
-- 锁被别人持有,返回剩余过期时间
return redis.call('pttl', KEYS[1]);
数据结构示例:
Redis中的数据:
myLock (Hash)
└─ "UUID-001:Thread-1" → 2 (重入了2次)
命令演示:
> HGETALL myLock
1) "8d7e3c9a-280e-4c2f-8b1e-9c5f2a3b4d5e:1"
2) "2"
> TTL myLock
(integer) 25 (还剩25秒过期)
释放锁脚本
-- KEYS[1]: 锁的key
-- ARGV[1]: 释放锁的channel
-- ARGV[2]: 解锁消息
-- ARGV[3]: 唯一标识
-- 检查锁是否存在
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil; -- 锁不存在或不属于当前线程
end;
-- 锁存在,计数器-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
-- 还有重入,只减计数器,不删除锁
redis.call('pexpire', KEYS[1], ARGV[1]);
return 0;
else
-- 计数器为0,删除锁
redis.call('del', KEYS[1]);
-- 发布锁释放消息
redis.call('publish', ARGV[1], ARGV[2]);
return 1;
end;
return nil;
场景演示:
场景1:重入2次
myLock → "UUID:Thread-1" : 2
第1次unlock:
counter = 2 - 1 = 1
→ 不删除锁,只刷新过期时间
第2次unlock:
counter = 1 - 1 = 0
→ 删除锁,发布释放消息
🎯 第三章:Redisson的高级特性
特性1:可重入锁
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 第一次加锁,计数器 = 1
System.out.println("外层业务");
lock.lock(); // 可重入!
try {
// 第二次加锁,计数器 = 2
System.out.println("内层业务");
} finally {
lock.unlock(); // 计数器 = 1
}
} finally {
lock.unlock(); // 计数器 = 0,锁被释放
}
特性2:自动续期(看门狗)
RLock lock = redisson.getLock("myLock");
// 不指定leaseTime,自动启用看门狗
lock.lock();
try {
// 即使业务执行100秒,锁也不会过期!
Thread.sleep(100000);
} finally {
lock.unlock();
}
// 如果手动指定leaseTime,看门狗不会启动
lock.lock(10, TimeUnit.SECONDS); // 10秒后自动释放,没有续期
特性3:尝试加锁(非阻塞)
RLock lock = redisson.getLock("myLock");
// 尝试加锁,最多等待10秒,锁30秒后自动释放
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
} else {
// 获取锁失败,执行降级逻辑
System.out.println("系统繁忙,请稍后再试");
}
特性4:公平锁
// 普通锁:抢占式,后来的可能先获得
RLock lock = redisson.getLock("myLock");
// 公平锁:先来先得(FIFO)
RLock fairLock = redisson.getFairLock("myFairLock");
fairLock.lock();
try {
// 保证按请求顺序执行
} finally {
fairLock.unlock();
}
公平锁原理:
Redis中维护一个有序队列:
myFairLock:queue (ZSet)
└─ "Thread-1" → 时间戳1
└─ "Thread-2" → 时间戳2
└─ "Thread-3" → 时间戳3
只有队列头部的线程能获取锁!
特性5:读写锁
RReadWriteLock rwLock = redisson.getReadWriteLock("myRWLock");
// 读锁(共享锁):多个线程可以同时持有
RLock readLock = rwLock.readLock();
readLock.lock();
try {
// 读操作
String data = getData();
} finally {
readLock.unlock();
}
// 写锁(排他锁):只有一个线程可以持有
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
// 写操作
setData("new value");
} finally {
writeLock.unlock();
}
💼 第四章:生产环境最佳实践
实践1:合理配置看门狗时间
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379");
// 设置看门狗超时时间(默认30秒)
config.setLockWatchdogTimeout(30000); // 30秒
RedissonClient redisson = Redisson.create(config);
选择建议:
业务执行时间 < 10秒:使用默认30秒
业务执行时间 10-30秒:设置60秒
业务执行时间 > 30秒:
- 优先优化业务逻辑
- 或者拆分为多个小任务
实践2:异常处理
@Service
public class SafeLockService {
@Autowired
private RedissonClient redisson;
public void safeExecute(String lockKey, Runnable business) {
RLock lock = redisson.getLock(lockKey);
try {
// 尝试加锁,最多等待10秒,锁30秒后自动释放
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取锁超时");
}
try {
// 执行业务
business.run();
} catch (Exception e) {
log.error("业务执行失败", e);
throw e;
} finally {
// 🔥 重要:一定要在finally中释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("加锁被中断", e);
}
}
}
实践3:监控和告警
@Component
public class LockMonitor {
@Autowired
private RedissonClient redisson;
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void monitorLocks() {
// 获取所有锁的key
RKeys keys = redisson.getKeys();
Iterable<String> lockKeys = keys.getKeysByPattern("*lock*");
for (String key : lockKeys) {
RLock lock = redisson.getLock(key);
// 检查锁是否被长时间持有
if (lock.isLocked()) {
long remainTime = lock.remainTimeToLive();
if (remainTime > 60000) { // 超过1分钟
log.warn("锁被长时间持有:key={}, remainTime={}ms",
key, remainTime);
// 发送告警...
}
}
}
}
}
实践4:分段锁(提高并发)
@Service
public class SegmentLockService {
@Autowired
private RedissonClient redisson;
private static final int SEGMENT_COUNT = 16; // 分16段
/**
* 根据productId哈希,路由到不同的锁
*/
public void deductStock(Long productId, Integer quantity) {
// 计算分段
int segment = (int) (productId % SEGMENT_COUNT);
String lockKey = "stock:segment:" + segment;
RLock lock = redisson.getLock(lockKey);
lock.lock();
try {
// 业务逻辑
doDeduct(productId, quantity);
} finally {
lock.unlock();
}
}
}
效果:
原来:1个锁,QPS = 1000
现在:16个锁,QPS = 16000(理想情况)
🎓 第五章:面试高分回答
问题:Redisson的看门狗机制是怎么实现的?
标准回答:
"Redisson的看门狗机制是为了解决Redis锁过期时间难以设置的问题。
工作原理:
- 当调用lock()方法不传leaseTime时,默认使用30秒过期时间,并启动看门狗
- 看门狗是一个定时任务,每隔10秒(lockWatchdogTimeout/3)执行一次
- 定时任务会检查锁是否还存在,如果存在就续期到30秒
- 当业务执行完,调用unlock()时,会停止看门狗定时任务
底层实现:
- 使用Netty的HashedWheelTimer创建定时任务
- 续期操作通过Lua脚本保证原子性
- 使用ThreadLocal存储定时任务引用,释放锁时取消任务
使用建议:
- 如果业务执行时间可预估,建议手动设置leaseTime,不启用看门狗
- 如果业务执行时间不确定,使用看门狗机制
- 注意一定要在finally中释放锁,否则看门狗会一直续期"
常见追问
Q1:看门狗续期失败怎么办?
A:续期失败的原因:
1. Redis服务器宕机
2. 网络故障
3. 锁已经被删除(比如Redis内存不足被LRU淘汰)
处理方式:
1. Redisson会记录续期失败,但不会抛异常
2. 下次续期时如果锁不存在,会停止看门狗
3. 业务层面应该有幂等性保证,即使锁失效也不影响数据一致性
Q2:如果业务执行完忘记unlock会怎样?
A:后果:
1. 看门狗会一直续期,锁永远不会释放
2. 其他线程永远获取不到锁
3. 造成资源泄漏
防范措施:
1. 一定要在finally中unlock
2. 使用try-with-resources模式
3. 设置全局监控,检测长时间持有的锁
4. 代码review时重点检查
Redisson的保护机制:
- 如果JVM进程退出,Redis连接断开,看门狗线程自然停止
- 但如果进程没退出,只是业务线程卡住,看门狗会一直续期
Q3:Redisson的锁为什么使用Hash结构而不是String?
A:使用Hash结构的好处:
1. 支持可重入:Hash的value可以记录重入次数
2. 支持多字段:可以存储更多元数据(如线程ID、时间戳等)
3. 原子操作:HINCRBY可以原子地增减计数
数据结构对比:
String: "myLock" → "UUID:ThreadId" (只能记录是否持有)
Hash: "myLock" → {"UUID:ThreadId": 2} (可以记录重入次数)
🎁 总结
核心要点
- 看门狗 = 定时任务 + Lua脚本续期
- 默认30秒过期,每10秒续期一次
- 手动指定leaseTime不会启动看门狗
- 一定要在finally中unlock
一句话记住
Redisson的看门狗就像一个贴心的保姆,定时检查你的锁还需要吗,需要就自动续费,你离开时自动停止服务!🐕
📚 源码阅读推荐
org.redisson.RedissonLock- 锁的核心实现org.redisson.command.CommandExecutor- 命令执行器org.redisson.client.codec.Codec- 序列化编解码org.redisson.connection.ConnectionManager- 连接管理
记住:理解原理比记住API更重要!🎯
祝你面试顺利!💪✨