Redisson的看门狗watchDog机制
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秒钟之内就会执行完毕,完全可以取消这个看门狗机制,来提升一部分性能。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了