
👨💻程序员三明治 :个人主页
🔥 个人专栏 : 《设计模式精解》 《重学数据结构》
🤞先做到 再看见!
bash
> set lock:swz threadId ex 5 nx ok
.....do something
> del lock:swz
大家肯定都用过上面的这种set分布式锁的命令,那这种命令会有什么问题呢?
毫无疑问,误删的问题。也就是我线程A的业务逻辑还没执行完却到了超时时间导致锁释放,释放之后线程B拿到了锁,然后此时线程A的业务逻辑执行完以后要del释放锁,就会把线程B的锁删掉。

解决方式:


这里static会使同一个jvm的所有线程的uuid一样,但是线程id本身是不一样的,而两个jvm的线程id有可能相同,但是uuid不一样
判断锁标识和释放锁是两个操作,如何保证原子性?
因为判断完锁标识之后在释放锁之前有可能遇到full gc导致阻塞,如果阻塞时间超过了超时时间,所就被自动释放。等我不阻塞了再去del锁就可能会把其他线程的锁删掉。
lua脚本可以解决:



Redisson是如何实现分布式锁的?
Redisson通过Lua脚本执行加锁操作。Lua脚本会判断锁是否存在,如果不存在,使用 hset 命令设置锁的值,使用 pexpire 命令设置锁的过期时间。如果锁存在而且当前线程持有,则将锁的值加1,并设置锁的过期时间为指定的时间。
两个图分别是获取锁和释放锁的流程
Redisson加锁的源码

释放锁的源码
解决问题一:不可重入
set命令的分布式锁不可以实现锁重入,因为同一个线程setnx都是一样的
故可以参考ReentrantLock实现锁重入的原理,每次相同的线程又一次来获取锁就对state变量值进行++操作
分布式锁Redisson和自定义分布式锁的一些区别:底层是hash结构,因为value部分不仅仅要存储"获取到锁的线程标识",还要存储"当前线程重入的次数"(支持锁重入的原理).
解决问题二:不可重试
set命令的分布式锁获取锁失败之后会立即返回false,不会重试。
源码上tryLock()API中传入了最大等待时间,则说明开启了失败重试机制
通过源码分析,当第一次获取锁失败后且还处于等待时间时,不会立马再去尝试获取锁,而是先去订阅,订阅别人释放锁的信号,保证当有人执行完释放锁的lua脚本后它能监听到。但是一旦等待的时间超过了最大等待时间waitTime,就会取消订阅,不会重试了。
订阅之后会在while(true)的一个死循环中不断的等待 尝试 等待 尝试...
在释放锁的源码lua中,释放锁最后会发布一个释放锁的信号,这些订阅了的线程就会监听到!!!
解决问题三:超时释放(看门狗机制)
只有获取锁成功了才会有leasetime的事情
- 获取锁成功且当leaseTime为-1时(默认)[-1才会走看门狗的逻辑]:
如果获取锁成功,会有一个自动更新过期时间的一个函数.
Redisson有一个自动续约更新的静态变量map,专门用于存储Redisson的不同锁的实例,就是Redisson创建的各个锁的实例都会被放到Redisson是静态变量map中,以锁的名字为key,每个锁创建的对应的Entry为value存入。达到的效果就是只要是同一个锁实例,不管来几次,将来拿到的永远是同一个entry。每一个锁实例都有自己对应的一个entry,entry中存放了threadId和定时任务,这个定时任务就是每隔10s会递归的调用自己,在单层递归的逻辑中完成对于锁过期时间的刷新...保证锁永不过期。直到当这把锁被unlock()成功之后,会从map中剔除锁实例,并清除锁对应的entry中存储的threadId和定时任务...
照这样锁什么时候释放???在unlock锁成功释放之后,有一个回调函数,会取消这个定时的更新任务. - 获取锁成功且当leasetime不为-1时(自己传值)
如果获取锁成功后,leasetime传入的不是-1,而是一个具体的时间,比如20s,那么Redisson会在20s后自动释放该锁,即使业务阻塞时间超过20s,锁也会被释放。在这种情况下,Redisson并不会启动开门狗机制,因为它知道锁的过期时间是20s,所以不需要再去刷新锁的过期时间。如果你希望锁一直有效,可以将leasetime设置为-1,这样Redisson会开启开门狗机制,确保锁一直有效,直到被释放。
java
// 看门狗续约核心源码
private void renewExpiration() {
// 从续约Map中获取当前锁的条目
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return; // 条目不存在,说明锁已释放
}
// 创建定时任务,在租期的1/3时间后执行续约
Timeout task = commandExecutor.getConnectionManager().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; // 没有线程持有锁,停止续约
}
// 🎯 执行异步续约操作
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
// 续约异常,记录日志并停止
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// 🎯 续约成功,递归调用继续下一次续约
renewExpiration();
} else {
// 🎯 续约失败,取消续约任务
cancelExpirationRenewal(threadId);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 🎯 默认10秒后执行
ee.setTimeout(task); // 保存定时任务引用
}
锁释放时清理看门狗
java
void cancelExpirationRenewal(Long threadId) {
ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (task == null) {
return;
}
if (threadId != null) {
task.removeThreadId(threadId);
}
if (threadId == null || task.hasNoThreads()) {
// 取消定时任务
task.getTimeout().cancel();
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
java
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 如果未指定leaseTime,使用看门狗机制
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// 锁获取成功,启动看门狗续期
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
Redisson分布式锁原理图

如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!