详解Redis锁误删、原子性难题及Redisson加锁底层原理、WatchDog续约机制

👨‍💻程序员三明治个人主页
🔥 个人专栏 : 《设计模式精解》 《重学数据结构》

🤞先做到 再看见!


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的事情

  1. 获取锁成功且当leaseTime为-1时(默认)[-1才会走看门狗的逻辑]:
    如果获取锁成功,会有一个自动更新过期时间的一个函数.
    Redisson有一个自动续约更新的静态变量map,专门用于存储Redisson的不同锁的实例,就是Redisson创建的各个锁的实例都会被放到Redisson是静态变量map中,以锁的名字为key,每个锁创建的对应的Entry为value存入。达到的效果就是只要是同一个锁实例,不管来几次,将来拿到的永远是同一个entry。每一个锁实例都有自己对应的一个entry,entry中存放了threadId和定时任务,这个定时任务就是每隔10s会递归的调用自己,在单层递归的逻辑中完成对于锁过期时间的刷新...保证锁永不过期。直到当这把锁被unlock()成功之后,会从map中剔除锁实例,并清除锁对应的entry中存储的threadId和定时任务...
    照这样锁什么时候释放???在unlock锁成功释放之后,有一个回调函数,会取消这个定时的更新任务.
  2. 获取锁成功且当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分布式锁原理图

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

相关推荐
chenzhou__3 小时前
MYSQL学习笔记(个人)(第十五天)
linux·数据库·笔记·学习·mysql
自由的疯3 小时前
Java 怎么学习Kubernetes
java·后端·架构
自由的疯3 小时前
Java kubernetes
java·后端·架构
一只自律的鸡4 小时前
【MySQL】第二章 基本的SELECT语句
数据库·mysql
普通网友4 小时前
IZT#73193
java·php·程序优化
rechol4 小时前
C++ 继承笔记
java·c++·笔记
liliangcsdn5 小时前
如何使用python创建和维护sqlite3数据库
数据库·sqlite
Han.miracle7 小时前
数据结构——二叉树的从前序与中序遍历序列构造二叉树
java·数据结构·学习·算法·leetcode
Le1Yu8 小时前
分布式事务以及Seata(XA、AT模式)
java