Redisson的看门狗watchDog机制是怎么实现的?

文章目录

  • INFO
  • 一、回顾
  • 二、WatchDog
    • 1、啥意思
    • 2、原理
  • 三、总结

INFO

作者: 编程界的小学生

日期: 2021/09/09

修订: 初版,未修订。2021/09/09

版权: 内部资料,切勿泄漏,违者必究。

一、回顾

上一篇讲解了加锁的核心流程、可重入是怎么做的以及互斥性是怎么实现的,但是如果业务代码没执行完锁却过期了,这时候怎么办?这不就线程不安全了吗?别急,Redssion内部有个看门狗机制,WatchDog!

二、WatchDog

1、啥意思

如果业务代码没执行完,锁却过期了,这时候其他线程又能抢锁了,线程不安全啦。所以Redisson内部有个看门狗的机制,意思是定时监测业务是否执行结束,没结束的话你这个锁是不是快到期了(超过锁的三分之一时间,比如设置的9s过期,现在还剩6s到期),那就重新续期。这样防止如果业务代码没执行完,锁却过期了所带来的线程不安全问题。

2、原理

回顾下怎么加锁的?lock()!

RLock lock = redisson.getLock("myLock");
lock.lock();

lock()干了啥?

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    // 加锁成功
    if (ttl == null) {
        return;
    }
    // 加锁失败,while(true)等待重试。
}

可以看到lock主要是请求tryAcquire(-1, -1, null, threadId)来完成加锁逻辑,然后判断加锁成功与否,失败的话就重试。目前还没发现watchDog的机制,那我们继续追下去,看看如何加锁的?

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

// watchDog机制在这里
// -1, -1, null,threadId
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Boolean> ttlRemainingFuture;
    // 省略一些请求lua加锁代码。之前都分析过。

    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        // ttlRemaining为true代表加锁成功。
        if (ttlRemaining == null) {
      			// 接下来是什么鬼逻辑?
            // leaseTime == -1就scheduleExpirationRenewal开启看门狗续期,
            // leaseTime != -1就不续期,只是把internalLockLeaseTime时间变成传进来的时间。
            // 这里疑点重重:
            // 1.什么时候leaseTime != -1?
            // 2.不是所有的lock()方法都有看门狗机制?
            if (leaseTime != -1) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

意外收获!好像不是所有的lock()都有看门狗,因为看到条件判断leaseTime == -1的时候才开启看门狗线程,不等于-1的时候就没有这个机制。那什么时候不等于-1呢?

回答这个问题前我们可以先推测下,-1是哪来的?是lock()入口默认带来的:

@Override
public void lock() {
    try {
        // -1 !!!
        lock(-1, null, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

那我们是不是懂了?他绝逼有带时间参数的lock()方法。确实有:

@Override
public void lock(long leaseTime, TimeUnit unit) {
    try {
        lock(leaseTime, unit, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

搜噶,原来用户加锁lock的时候可以自定义时间,这个时间是干嘛的?加锁成功后等待这个时间就过期,不管你业务是否执行完成,因为没有看门狗机制。比如你设置的1s,业务执行了2s,那中途锁过期了,不会续期,可能造成线程不安全。

现在知道watchDog何时生效了,那继续看下他是怎么工作的?

上文可以发现续期的代码在这个方法里面:scheduleExpirationRenewal(threadId);这个方法底层是靠renewExpiration来完成续期的。

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
	
    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;
            }
			// 调用lua脚本进行续期
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                // 报异常就移除key
                if (e != null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
				// 续期成功的话就下一轮续期。
                if (res) {
                    // reschedule itself
                    renewExpiration();
                } else {
                    // 续期失败的话就取消续期,移除key等操作
                    cancelExpirationRenewal(null);
                }
            });
        }
        // 这里是个知识点,续期线程在过期时间达到三分之一的时候工作,比如9s过期时间,那么续期会在第3秒的时候工作,也就是还剩余6s的时候进行续期
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    ee.setTimeout(task);
}

这里有四个关键点:

  • 续期核心lua脚本在renewExpirationAsync里
  • 续期成功自己调用自己,也就是为下一次续期做准备
  • 续期失败就取消续期,移除key等操作
  • 续期的开始时间是超过过期时间的三分之一,比如9s过期时间,那么第3s的时候开始续期。

所以重点看下续期的lua源代码:

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

很简单,就是看当前线程有没有加锁hexists, KEYS[1], ARGV[2]) == 1,有加锁的话就代表业务线程还没执行完,就给他的锁重新续期pexpire', KEYS[1], ARGV[1],然后返回1,也就是true,没加锁的话返回0,也就是false。

那就是返回1就调用自己准备下一次续期:renewExpiration();,返回0就调用cancelExpirationRenewal(null);取消续期,删除key等操作。

三、总结

Redisson的看门狗watchDog机制是怎么实现的?_第1张图片

需要注意的点:

  • watchDog并不是全部lock都生效,而是lock没设置过期时间的那些锁才会开启watchDog续期,没设置过期时间的话默认采取的是watchDog的30s过期时间。如果调用lock(time,unit)是不会开启watchDog线程续期的,是有可能造成线程不安全的。
  • 续期是段lua脚本。
  • 续期线程会在续期时间超过三分之一的时候执行。

疑问:不会浪费性能吗?每个方法都起个看门狗线程,这个影响有多大?

此篇讲述了看门狗是怎么工作的,他的核心原理我们一清二楚,整个加锁的这块流程算是告一段落了,接下来我们需要知道锁是怎么释放的?下篇分析!

你可能感兴趣的:(从0到1玩转分布式锁底层原理,java,分布式,分布式锁,redis,redisson)