Redisson 分布式锁实现原理(Redisson 3.13.X)

79 阅读4分钟

这里我们主要看一下,JDK11下的Redisson,是如何实现加锁和进行锁的续期的

核心流程源码分析

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 尝试获取锁,如果拿到了锁,返回null,没到到锁,则返回当前锁的剩余时间
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return true;
        }
        // 更新剩余等待时间
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            // 等待超时,处理获取锁失败
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
        
        current = System.currentTimeMillis();
        // 订阅锁释放的事件
        RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        // 在这里阻塞掉当前等待拿锁的线程,如果超时,返回false
        if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
            //对等待超时的处理
            if (!subscribeFuture.cancel(false)) {
                subscribeFuture.onComplete((res, e) -> {
                    if (e == null) {
                        //取消订阅
                        unsubscribe(subscribeFuture, threadId);
                    }
                });
            }
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

        try {
            //重新计算等待时间
            time -= System.currentTimeMillis() - current;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
            // 循环拿锁
            while (true) {
                long currentTime = System.currentTimeMillis();
                ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    return true;
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }

                // waiting for message
                currentTime = System.currentTimeMillis();
                //等待锁释放的事件
                
                if (ttl >= 0 && ttl < time) {
                    subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
            }
        } finally {
            // 取消锁消息的订阅
            unsubscribe(subscribeFuture, threadId);
        }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
    }

加锁和续期核心代码

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                "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]);",
                Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

首先先看各个属性是什么:

  • KEYS[1] 存的是你的定义的redis锁的key
  • ARGV[1] 存的是你的持有锁的时间,也就是leaseTime
  • ARGV[2] 存的是你的threadId

然后看看这个lua脚本干了什么: 第一个if:

  • 判断你的redis锁的key是否存在
    • 如果不存在,先给key下的ARGV[2]+1
    • 然后给你的key下的ARGV[1]添加超时时间
    • 然后返回nil,也就是null

这里有一个问题,就是threadId是被包装过的getLockName(threadId),这里填进去的值,其实是客户端的uuid+threadId

第二个if:

  • 第一个key已经存在了,那么看这个key里ARGV[2]存的值是否等于客户端的uuid+threadId,这里其实就是一个可重入锁的实现了
    • 如果等于当前线程,则给ARGV[2]+1
    • KEYS[1],重新设置持锁时间,这里其实就已经刷新了锁的持锁时间
    • 然后返回nil,也就是null
  • 如果上面的if都不成立,则返回当前key的剩余失效时间

总结一下上面的lua脚本就是,这里定义了一个可重入锁,去拿锁的时候,没有锁就直接拿锁,设置持锁时间;有锁则判断是否当前线程持有,是的话 就刷新当前的持锁时间,给ARGV[2]+1,如果拿不到锁,就返回锁的剩余时间

自动续期实现

redisson看门狗自动续期又是如何实现的呢?一般看门狗机制启动,是在获取到锁以后,我们直接看tryAcquire()方法里的tryAcquireAsync()方法

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        // 当你的持锁时间不为-1时,是不会触续期的,因为拿锁后就直接返回了
        if (leaseTime != -1) {
            return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining == null) {
                //自动续期的逻辑
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }

这里可以看到,你的leaseTime只要不是-1,是不会触发自动续期的操作的,下面看一下自动续期的逻辑

private void renewExpiration() {
        //EXPIRATION_RENEWAL_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;
                }
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }
                // 续期的逻辑,主要检查锁是不是由当前线程持有,是的话返回true
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getName() + " expiration", e);
                        return;
                    }
                    
                    if (res) {
                        // reschedule itself
                        // 递归自己
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }

上面的代码可以看到,Redisson内部维护了一个需要续期的Map,通过去这个map中找当前线程的锁,还在不在这个map里,如果在,就给这个key续期, 不在了,那么内部的递归就有出口了,可以跳出递归的循环

总结

Redisson是如何实现分布式锁以及锁的自动续期的?

  • 首先是Lua脚本,保证加锁操作操作的原子性
  • 拿锁失败的线程订阅所释放的事件,来进行线程的阻塞和唤醒
  • 维护一个需要续期的Map对象EXPIRATION_RENEWAL_MAP,用于存储需要续期的对象
  • 续期时,在EXPIRATION_RENEWAL_MAP根据当前线程id查有没有值,有,说明需要续期,通过Lua脚本给锁续期,然后就递归自己,重新走一遍逻辑, 直到当前线程在EXPIRATION_RENEWAL_MAP拿不到值为止
  • 在释放锁的时候,会删除当前线程在EXPIRATION_RENEWAL_MAP中的对象