Redisson分布式锁原理的探究,不是你想的那样

Redisson分布式锁原理的探究,不是你想的那样

背景

作为Java开发,聊到分布式锁,大家第一反应该都会想到 Redis吧,基本思路用SetNX + Lua脚本实现,但都有些问题例如锁过期了业务还没执行完、释放别人的锁。

理想的方案是用Redisson,网上的说法是Redission加锁后,会有一个异步线程定时检查,如果锁还存在,则给锁续期。

如果你没看过具体实现,你肯定有疑问

问题

  • Redisson 是加一把锁成功后,就另起一个异步线程自动续期吗?
  • 锁释放后,续期任务如何取消呢?

带着问题分析源码

总体

测试代码

ini 复制代码
  RLock lock = redissonClient.getLock("分布式锁");
  lock.tryLock();

tryLock() 方法一直往下点,最终来到 RedissonLock类的 tryAcquireOnceAsync()方法,我已对核心逻辑做了注释

scss 复制代码
  private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
         RFuture<Boolean> acquiredFuture;
         //如果手动设置了锁的过期时间
         if (leaseTime > 0) {
             acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
         } else {
             //这是默认加锁逻辑,也就是默认设置锁30s
             acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                     TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
         }
         
         //异步编程
         CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {
            //如果加锁成功
             if (acquired) {
                 if (leaseTime > 0) {
                     internalLockLeaseTime = unit.toMillis(leaseTime);
                 } else {
                     //默认逻辑:加锁成功后,启动一个给锁的续期任务(看门狗)
                     scheduleExpirationRenewal(threadId);
                 }
             }
             return acquired;
         });
         return new CompletableFutureWrapper<>(f);
     }

加锁原理

继续查看源代码 默认加锁的方法 。

ini 复制代码
 acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                     TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
                     
                     
                     
 if (redis.call('exists', KEYS[1]) == 0) then " +
   "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 " +
   "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
   "redis.call('pexpire', KEYS[1], ARGV[1]); " +
   "return nil; " +
   "end; " +
   "return redis.call('pttl', KEYS[1]);             

可以看到,加锁是通过Hash结构Lua脚本实现。 其中Hash的 key是锁的名称,field是线程名字,value是所重入次数。

加锁逻辑如下

  1. 判断 分布式锁名称 key是否存在

    1. 如果不存在,则设置 这个锁,然后所重入次数 + 1。
  2. 如果存在,则判断field 是否为当前线程

    1. 如果不是,说明别的线程已经抢到锁了。直接返回加锁失败
    2. 如果是,则重入次数加一。

续期原理-----看门狗机制

scheduleExpirationRenewal(threadId) 方法里的 renewExpiration() 方法。 这是源码,注释我已写好

ini 复制代码
 private void renewExpiration() {
         //
         ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
         if (ee == null) {
             return;
         }
         
         //这段代码的含义,就是向时间轮添加一个10s的延迟任务,具体就是给锁续期。
         Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
             @Override
             public void run(Timeout timeout) throws Exception {
                 
                 //这是一个ConcurrentHashMap,再前文加锁成功后,会向里面put()一个值
                 ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                 if (ent == null) {
                     return;
                 }
                 Long threadId = ent.getFirstThreadId();
                 if (threadId == null) {
                     return;
                 }
                 CompletionStage<Boolean> future = renewExpirationAsync(threadId);
                 future.whenComplete((res, e) -> {
                     if (e != null) {
                         log.error("Can't update lock " + getRawName() + " expiration", e);
                         // 从map里移除相关信息,执行任务时发现为 null,就退出方法。
                         EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                         return;
                     }   
                      //如果业务还在执行中,则递归执行该方法,再向时间轮添加一个续期任务。
                     if (res) {
                         renewExpiration();
                     } else {
                         cancelExpirationRenewal(null);
                     }
                 });
             }
         }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
         
         ee.setTimeout(task);
     }

默认 key 过期时间 30s,然后定时任务每 10 秒( lockWatchdogTimeout/3 )进行一次调用,执行锁续期动作,若这个线程还持有这个锁,就对这个线程持有的锁进行续期操作(通过 pexpire 续期 key 30s)。

当业务执行完,解锁操作时,会调用ConcurrentHashMap.remove( getEntryName()),这样续期方法就直接退出了。

总体图

回到最初的问题

  • Redisson 是加一把锁成功后,就另起一个异步线程自动续期吗?

很明显不是,一个线程管理所有分布式锁的续期任务

  • 锁释放后,续期任务如何取消呢?

从map里移除相关信息,执行任务时发现为 null,就退出方法。(在上面的代码注释已经写得很清楚了)

相关推荐
晨曦_子画几秒前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
假装我不帅20 分钟前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
南宫生23 分钟前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
神仙别闹23 分钟前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
Heavydrink36 分钟前
HTTP动词与状态码
java
ktkiko1139 分钟前
Java中的远程方法调用——RPC详解
java·开发语言·rpc
计算机-秋大田1 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
神里大人1 小时前
idea、pycharm等软件的文件名红色怎么变绿色
java·pycharm·intellij-idea
货拉拉技术1 小时前
货拉拉-实时对账系统(算盘平台)
后端
小冉在学习1 小时前
day53 图论章节刷题Part05(并查集理论基础、寻找存在的路径)
java·算法·图论