Redisson的看门狗watchDog机制

发布时间 2023-09-13 23:35:58作者: 程序侠

Redisson的看门狗watchDog机制

如果业务代码没执行完锁却过期了,这时候怎么办?

这不就线程不安全了吗?

别急,Redssion内部有个看门狗机制,WatchDog!

Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。

默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

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何时生效了,那继续看下他是怎么工作的?

上文可以发现续期的代码在这个方法里面:scheduleExpirationRenewal(threadId);

这个方法底层是靠renewExpiration来完成续期的。

private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;    }
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic 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) -> {// 报异常就移除keyif (e != null) {log.error("Can't update lock " + getRawName() + " expiration", e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;                }// 续期成功的话就下一轮续期。if (res) {// reschedule itselfrenewExpiration();                } 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如果只是用lock.lock();

不传过期时间的话,会启动看门狗机制,传过期时间的话,就不会启动看门狗机制。

  • watchDog并不是全部lock都生效,而是lock没设置过期时间的那些锁才会开启watchDog续期,没设置过期时间的话默认采取的是watchDog的30s过期时间。

    如果调用lock(time,unit)是不会开启watchDog线程续期的,是有可能造成线程不安全的。

  • 续期是段lua脚本。

  • 续期线程会在续期时间超过三分之一的时候执行。

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

看门狗的性能问题

很多小伙伴都认为看门狗是非常消耗性能的,其实性能的确是会有一些消耗,但是没有很多。

前几天有个小伙伴抛出了一个疑问:假如说每个线程都启动一个TimerTask来不断刷新过期时间,岂不是服务器很快就“炸了”?

其实不然,只有抢占到锁的线程才会开启看门狗,并不是每个等待的线程都会开启一个看门狗。

也就是说——基本上每一个锁会对应一个看门狗,而不是每一个线程对应一个看门狗。

这样看来,是不是性能浪费的就不是很多了?

其实看门狗机制主要是用于业务代码执行时间忽长忽短的,如果一个业务代码,我们确定它在10秒钟之内就会执行完毕,完全可以取消这个看门狗机制,来提升一部分性能。

参考文章:https://mp.weixin.qq.com/s/e33jWlPEnsfcBse7TjW1Yg