Redisson分布式锁自动续期源码分析

2,568 阅读3分钟

我们目前项目中使用的 redis 锁并没有续期的功能,所以在执行长时间任务时会出现异常,解决这个问题比较正确的姿势是采用 redisson 这个客户端工具.具体介绍可以搜索最大的同性交友网站 github.

我们看官方的解释:

图片

Redisson 为避免存储这个分布式锁的Redisson节点宕机出现锁死的情况,在内部提供了一个监控锁的看门狗,其作用是在Redisson实例被关闭前,不断延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒,可以通过修改 Config.lockWatchdogTimeout 来指定。另外Redisson还通过加锁的方法提供了 leaseTime 的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

细心的同学可能会发现上面解释的歧义:

  1. 看门狗每隔 30 秒检查一次锁的超时时间

  2. 看门狗会去检查锁的超时时间,锁的时间时间默认是30秒

所以看门狗具体是怎么检查锁的,我们从源码中找答案。代码如下:

    // Demo.java
    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");

        RedissonClient redisson = Redisson.create(config);
        RLock lock = redisson.getLock("anyLock");

        lock.lock();
    }

    // Redisson.java
    // 先看getLock()方法
    public RLock getLock(String name) {
        return new RedissonLock(connectionManager.getCommandExecutor(), name);
    }

    // RedissonLock.java
     public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.id = commandExecutor.getConnectionManager().getId();
        // 从这里我们知道,internalLockLeaseTime 和 lockWatchdogTimeout这两个参数是相等的.
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        this.entryName = this.id + ":" + name;
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }

    // Config.java
    private long lockWatchdogTimeout = 30 * 1000;

上面代码我们看到锁的释放时间就是看门狗的超时时间,默认为30秒,但还有一个问题,看门狗是多久来延长一次有效期呢?我们接着往下看:

继续看 lock() 方法

    //RedissonLock.java
    public void lock() {
        try {
            lock(-1, null, false);
        } catch (InterruptedException e) {
            throw new IllegalStateException();
        }
    }
    //RedissonLock.java
    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }
        ...
    }
    //RedissonLock.java
    private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
        return get(tryAcquireAsync(leaseTime, unit, threadId));
    }
    //RedissonLock.java
    private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        ...
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(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;
    }
    // RedissonLock.java
     private void scheduleExpirationRenewal(long threadId) {
        ExpirationEntry entry = new ExpirationEntry();
        ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);
            // 具体续期方法
            renewExpiration();
        }
    }
     // RedissonLock.java 具体续期方法
    private void renewExpiration() {
       ...
        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;
                }

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

    }

从上面分析我们就知道了,获取锁成功就会开启一个定时任务,也就是 watchdog,定时任务会定期检查去续期 scheduleExpirationRenewal(threadId). 这里定时用的是netty-common包中的 HashedWheelTimer,默认情况下,该定时调度每次调用的时间差是 internalLockLeaseTime/3.也就10秒.

总结:通过源码分析我们知道,默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒.那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了.