代码示例

@Slf4j
public class WatchDogDemo {

    static Config config = null;
    static Redisson redisson = null;
    static final String KEY_WATCH = "watch_dog_key";
    static{
        config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        redisson = (Redisson)Redisson.create();
    }

    public static void main(String[] args) {
        RLock lock = redisson.getLock(KEY_WATCH);
        lock.lock();
        try {
            log.info(">>>>>>>>11111.......abc");
            TimeUnit.SECONDS.sleep(25);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

流程解析

直接从 lock() 和 unlock() 看起,首先跟踪进入到 lock() 方法核心,如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_人人开源的分布式锁redis支持集群吗

继续进入到核心方法,如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_lua_02

如果自己不设置过期时间,那么底层默认会给你设置 30s 作为锁过期时间,如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_人人开源的分布式锁redis支持集群吗_03

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_分布式_04

进入到 tryLockInnerAsync() 加锁的方法如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_客户端_05

会发现就是通过三段 lua 脚本指令去加锁,其中 KEYS[1] 代表是你加锁的那个 key,ARGV[1] 代表过期时间(默认30s)、ARGV[2] 代表加锁的客户端 ID。

第一段 lua 脚本表示: 第一个客户端过来判断是否存锁 key 是否已经存在,第一次过来很明显不存在这个 key,所以需要去上锁,从而执行 hincrby 指令开始加锁,并且给这个客户端分配了一个唯一标识的客户端 ID (ARGV[2]) ,并且通过expire设置过期时间为 30s,然后最后返回 null。

第二段 lua 脚本表示:假如第二次过来的还是第一个客户端,判断已经存在锁了,那么就要去执行锁重入操作,给这个客户端的锁次数 +1,在释放锁的时候也要做相应的 -1 操作,然后最后返回 null。

第三段 lua 脚本表示:假设第二次过来的是另一个客户端,会先执行第一段判断第二个客户端是否要加锁,假设第一个客户端还没有释放掉锁,那么第二个客户端就不能加锁成功,只能去执行第三段 lua 指令,然后最后返回第一个客户端持有锁的过期时间,注意前两步都是返回的 null,只有最后一步是返回持有锁的过期时间。

可以总结出一句话,分布式锁的加锁和设置过期时间都是通过三段 lua 脚本指令实现的,一条指令的执行具备原子性。

接着继续分析下面一段代码,如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_分布式_06

通过 tryLockInnerAsync() 方法已经可以尝试加锁:

第一种情况:

如果加锁成功,然后 lua 脚本直接就会返回 null,所以 ttlRemaining 变量值就是 null,然后进入 if 逻辑。leaseTime 默认传入的值是 -1。所以走 else 分支的逻辑。

重点看这个 scheduleExpirationRenewal() 方法,如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_加锁_07

首先能够看到这是一个递归调用,自己调用自己,递归调用最容易出现的就是栈溢出问题,所以得给一个休息的时间,所以每隔 1/3 时间才会去执行一次,默认时间 30s,所以就是每隔 10s 执行一次,主要去干这样一件事情,重新刷新锁的过期时间

把这段代码简化成自己能看到的代码如下所示:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_加锁_08

然后主要是在刷新锁的过期时间(虽然已加锁成功就开启了 watchdog 监控🐶,但是并不是一开始就去刷新时间,而是过了 10s 才会去刷新这个过期时间)。进入 renewExpirationAsync() 方法如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_lua_09

会发现也是两段 lua 脚本指令:

第一段会去判断你这个锁是否还被人持有着,只要是这把锁还在被人使用,那么肯定不能让这把锁过期失效,所以要给这个把锁续命,或者说重新刷新回默认的 30s,ARGV[1] 就是表示一开始赋值的 30s 时间。最后一定要非常注意返回的是 1。

第二段自然就是表示锁没有人使用了呗,那么肯定就不用去做续命了,直接返回 0

然后继续往下看代码,如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_lua_10

如果 lua 返回 1,res 就是 true,如果 lua 返回 0,res 就是 false,返回 1 表示需要续命,那就会在此调用自己 renewExpiration() 方法,然后又去执行这段 lua 脚本重新刷新锁过期时间。

什么时候能够跳出呢?当锁不需要续命(过期或者释放了)的时候,那么这里就会返回 0,走 else 逻辑就可以跳出了,自己不去调用自己了不就可以退出了么。

第二种情况:

如果加锁不成功,然后 lua 脚本直接就会返回这个锁还有多少时间过期,所以 ttlRemaining 变量值不为 null。直接走 return CompletableFutureWrapper(f) 逻辑

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_人人开源的分布式锁redis支持集群吗_11

然后继续观察代码如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_lua_12

tryAcquire(-1, leaseTime, unit, threadId) 表示继续尝试去加锁是否能成功,假设还是失败的,继续看下图:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_客户端_13

ttl 返回值大于0 表示加锁失败,那么就要让这个线程不要这么频繁的去尝试了,等人家通知你吧,这里就使用到了 AQS 里面闭锁 Semaphore 信号量作为拦截,信号大小初始设置成 0 了,表示要让这个线程去睡眠了

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_lua_14

最终会调用 park() 方法线程被挂起,等着被唤醒(等着锁释放或者其他线程被中断了就会醒来)

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_加锁_15

最后再来看下锁释放的源码如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_人人开源的分布式锁redis支持集群吗_16

主要干两件事情,一是去释放锁肯定是去执行 lua 脚本指令,毕竟加锁都是使用的 lua,二是去取消 watchdog 的锁续命操作。

最后看下这个解锁的 lua 脚本如下:

人人开源的分布式锁redis支持集群吗 redisson分布式锁源码分析_分布式_17

也是分成对应的三段:

第一段是:如果发现当前线程和加锁线程不是同一个线程,不允许解锁,直接返回 null,解锁失败。

第二段是:每次调用 hincrby 递减 1,释放一次锁,如果发现剩余次数还大于 0 ,表示是自增锁,那就在把过期时间重新刷新下。

第三段是:调用 del 删除锁,并且还发布了删除消息,返回 1 表示释放锁成功。