Redisson分布式锁底层原理实现详解
Redisson分布式锁底层原理实现详解
Redisson是基于Redis实现的分布式锁,其核心思想是利用Redis的SET NX(SET if Not eXists)+ PX(过期时间)来实现锁的互斥性,同时通过Lua脚本处理加锁、解锁、续期等原子操作,保证分布式环境下的安全性。本文将深入解析Redisson分布式锁的底层实现原理,包括加锁、锁续约、解锁、可重入性以及失败重试等关键环节。
1、概述
Redisson是基于Redis实现的分布式锁,其核心思想是利用Redis的SET NX(SET if Not eXists)+ PX(过期时间)来实现锁的互斥性,同时通过Lua脚本处理加锁、解锁、续期等原子操作,保证分布式环境下的安全性。
2、主要过程
2.1、加锁(tryLock)
定义:通过SET NX实现锁的互斥功能。
- 命令:SET key value NX PX expireTime
- 作用:使用SETNX保证互斥性,使用PX设置过期时间防止死锁
- 数据结构:
- key:lock:{name},如lock:order:123
- value:存储唯一标识(UUID + 线程ID),用于区分不同客户端的锁
特点:
如果key不存在,创建并返回OK,表示加锁成功。
如果key存在,返回nil,表示加锁失败。
2.2、锁续约(Watchdog机制)
定义:在tryLock未设置leaseTime时候会启动看门狗机制,定期(默认10s)延长30s锁的过期时间。
实现方式:
Redisson在获取RFuture时候,如果获取锁成功,就会执行scheduleExpirationRenewal()方法,执行对应的代码scheduleExpirationRenewal代码如下:
// 续期锁的过期时间
private void renewExpiration() {
// 从过期续期映射中获取锁的过期条目
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return; // 如果找不到条目,说明没有锁需要续期
}
// 创建一个定时任务,用于定期续期锁的过期时间
Timeout task = commandExecutor.getServiceManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 重新获取过期条目
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return; // 如果条目已被移除,结束任务
}
// 获取持有锁的线程ID
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return; // 如果没有线程ID,说明没有线程持有该锁,结束任务
}
// 异步续期锁的过期时间
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
// 如果续期过程中发生错误,记录日志并移除续期条目
log.error("Can't update lock {} expiration", getRawName(), e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// 如果续期成功,重新调度续期任务
renewExpiration();
} else {
// 如果续期失败,取消续期操作
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 定时任务每 internalLockLeaseTime / 3 毫秒执行一次
// 设置定时任务到过期条目中
ee.setTimeout(task);
}
// 启动续期操作,首次获取锁时会调用此方法
protected void scheduleExpirationRenewal(long threadId) {
// 创建新的过期条目
ExpirationEntry entry = new ExpirationEntry();
// 尝试将新的条目加入到续期映射中
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
// 如果条目已存在,说明已有其他线程在续期,添加当前线程ID到条目中
oldEntry.addThreadId(threadId);
} else {
// 如果是首次添加,开始进行续期操作
entry.addThreadId(threadId);
try {
// 启动锁过期续期任务
renewExpiration();
} finally {
// 如果当前线程被中断,取消续期操作
if (Thread.currentThread().isInterrupted()) {
cancelExpirationRenewal(threadId);
}
}
}
}
2.3、解锁(unlock)
命令:通过Lua脚本确保原子性
2.4、可重入性
实现方式:Redisson维护一个可重入计数器(Hash结构),如果同一线程再次获取锁,计数器递增。
存储结构:
key:lock:{name}
value:UUID:threadId作为唯一标识
hash结构(可重入计数器)
HINCRBY lock:{name} UUID:threadId 1
解锁时,只有计数器==0,才真正删除Redis锁
2.5、失败重试
策略:
线程获取锁失败时,不会立即放弃,而是进入自旋等待。
主要机制:
通过计算计算减去锁消耗时间得到的锁最大等待时间,如果>0执行对应的订阅功能,订阅其他获取锁的信号,如果其他线程释放锁会发布信号,收到订阅后再执行重试获取锁机制,,等待订阅最大时间是waitime,从而不是直接进行循环,导致性能的损耗,最后进行while(true)直至获取锁成功为止。