likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 8

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

背景

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

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

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

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

问题

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

带着问题分析源码

总体

测试代码

  RLock lock = redissonClient.getLock("分布式锁");
  lock.tryLock();

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

  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);
     }

加锁原理

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

 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() 方法。 这是源码,注释我已写好

 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分布式锁原理的探究,不是你想的那样

回到最初的问题

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

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

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

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