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,就退出方法。(在上面的代码注释已经写得很清楚了)

相关推荐
Michael_lcf13 小时前
Java的UDP通信:DatagramSocket和DatagramPacket
java·开发语言·udp
摇滚侠13 小时前
Spring Boot 3零基础教程,WEB 开发 HttpMessageConverter @ResponseBody 注解实现内容协商源码分析 笔记33
java·spring boot·笔记
计算机毕业设计小帅14 小时前
【2026计算机毕业设计】基于Springboot的校园电动车短租平台
spring boot·后端·课程设计
调试人生的显微镜14 小时前
Web前端开发工具实战指南 从开发到调试的完整提效方案
后端
静心观复14 小时前
drawio画java的uml的类图时,class和interface的区别是什么
java·uml·draw.io
Java水解14 小时前
【SQL】MySQL中空值处理COALESCE函数
后端·mysql
Laplaces Demon14 小时前
Spring 源码学习(十四)—— HandlerMethodArgumentResolver
java·开发语言·学习
guygg8814 小时前
Java 无锁方式实现高性能线程
java·开发语言
ss27314 小时前
手写Spring第7弹:Spring IoC容器深度解析:XML配置的完整指南
java·前端·数据库
Python私教14 小时前
DRF:Django REST Framework框架介绍
后端·python·django