解讀Eureka的TimedSupervisorTask類(自動(dòng)調(diào)節(jié)間隔的周期性任務(wù))
起因
一個(gè)基于Spring Cloud框架的應(yīng)用,如果注冊(cè)到了Eureka server,那么它就會(huì)定時(shí)更新服務(wù)列表,
這個(gè)定時(shí)任務(wù)啟動(dòng)的代碼在com.netflix.discovery.DiscoveryClient類的initScheduledTasks方法中,
如下(來(lái)自工程eureka-client,版本1.7.0):
private void initScheduledTasks() { //更新服務(wù)列表 if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); scheduler.schedule( new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ), registryFetchIntervalSeconds, TimeUnit.SECONDS); } ... //略去其他代碼
上述代碼中,scheduler是ScheduledExecutorService接口的實(shí)現(xiàn),其schedule方法的官方文檔如下所示:
上圖紅框顯示:
該方法創(chuàng)建的是一次性任務(wù),但是在實(shí)際測(cè)試中,如果在CacheRefreshThread類的run方法中打個(gè)斷點(diǎn),就會(huì)發(fā)現(xiàn)該方法會(huì)被周期性調(diào)用;
因此問(wèn)題就來(lái)了:
方法schedule(Callable callable,long delay,TimeUnit unit)創(chuàng)建的明明是個(gè)一次性任務(wù),但CacheRefreshThread被周期性執(zhí)行了;
尋找答案
打開的run方法源碼,請(qǐng)注意下面的中文注釋:
public void run() { Future future = null; try { //使用Future,可以設(shè)定子線程的超時(shí)時(shí)間,這樣當(dāng)前線程就不用無(wú)限等待了 future = executor.submit(task); threadPoolLevelGauge.set((long) executor.getActiveCount()); //指定等待子線程的最長(zhǎng)時(shí)間 future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout //delay是個(gè)很有用的變量,后面會(huì)用到,這里記得每次執(zhí)行任務(wù)成功都會(huì)將delay重置 delay.set(timeoutMillis); threadPoolLevelGauge.set((long) executor.getActiveCount()); } catch (TimeoutException e) { logger.error("task supervisor timed out", e); timeoutCounter.increment(); long currentDelay = delay.get(); //任務(wù)線程超時(shí)的時(shí)候,就把delay變量翻倍,但不會(huì)超過(guò)外部調(diào)用時(shí)設(shè)定的最大延時(shí)時(shí)間 long newDelay = Math.min(maxDelay, currentDelay * 2); //設(shè)置為最新的值,考慮到多線程,所以用了CAS delay.compareAndSet(currentDelay, newDelay); } catch (RejectedExecutionException e) { //一旦線程池的阻塞隊(duì)列中放滿了待處理任務(wù),觸發(fā)了拒絕策略,就會(huì)將調(diào)度器停掉 if (executor.isShutdown() || scheduler.isShutdown()) { logger.warn("task supervisor shutting down, reject the task", e); } else { logger.error("task supervisor rejected the task", e); } rejectedCounter.increment(); } catch (Throwable e) { //一旦出現(xiàn)未知的異常,就停掉調(diào)度器 if (executor.isShutdown() || scheduler.isShutdown()) { logger.warn("task supervisor shutting down, can't accept the task"); } else { logger.error("task supervisor threw an exception", e); } throwableCounter.increment(); } finally { //這里任務(wù)要么執(zhí)行完畢,要么發(fā)生異常,都用cancel方法來(lái)清理任務(wù); if (future != null) { future.cancel(true); } //只要調(diào)度器沒(méi)有停止,就再指定等待時(shí)間之后在執(zhí)行一次同樣的任務(wù) if (!scheduler.isShutdown()) { //這里就是周期性任務(wù)的原因:只要沒(méi)有停止調(diào)度器,就再創(chuàng)建一次性任務(wù),執(zhí)行時(shí)間時(shí)dealy的值, //假設(shè)外部調(diào)用時(shí)傳入的超時(shí)時(shí)間為30秒(構(gòu)造方法的入?yún)imeout),最大間隔時(shí)間為50秒(構(gòu)造方法的入?yún)xpBackOffBound) //如果最近一次任務(wù)沒(méi)有超時(shí),那么就在30秒后開始新任務(wù), //如果最近一次任務(wù)超時(shí)了,那么就在50秒后開始新任務(wù)(異常處理中有個(gè)乘以二的操作,乘以二后的60秒超過(guò)了最大間隔50秒) scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS); } } }
真相就在上面的最后一行代碼中:
scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS)
執(zhí)行完任務(wù)后,會(huì)再次調(diào)用schedule方法,在指定的時(shí)間之后執(zhí)行一次相同的任務(wù),這個(gè)間隔時(shí)間和最近一次任務(wù)是否超時(shí)有關(guān),如果超時(shí)了就間隔時(shí)間就會(huì)變大;
總結(jié)
從整體上看,TimedSupervisorTask是固定間隔的周期性任務(wù),一旦遇到超時(shí)就會(huì)將下一個(gè)周期的間隔時(shí)間調(diào)大,如果連續(xù)超時(shí),那么每次間隔時(shí)間都會(huì)增大一倍,一直到達(dá)外部參數(shù)設(shè)定的上限為止,一旦新任務(wù)不再超時(shí),間隔時(shí)間又會(huì)自動(dòng)恢復(fù)為初始值,另外還有CAS來(lái)控制多線程同步,簡(jiǎn)潔的代碼,巧妙的設(shè)計(jì),值得我們學(xué)習(xí);
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Mybatis-plus多數(shù)據(jù)源配置的兩種方式總結(jié)
這篇文章主要為大家詳細(xì)介紹了Mybatis-plus中多數(shù)據(jù)源配置的兩種方式,文中的示例代碼簡(jiǎn)潔易懂,感興趣的小伙伴可以跟隨小編一起了解一下2022-10-10mybatis的mapper特殊字符轉(zhuǎn)移及動(dòng)態(tài)SQL條件查詢小結(jié)
mybatis mapper文件中條件查詢符,如>=,<,之類是不能直接寫的會(huì)報(bào)錯(cuò)的需要轉(zhuǎn)移一下,本文給大家介紹了常見的條件查詢操作,對(duì)mybatis的mapper特殊字符及動(dòng)態(tài)SQL條件查詢相關(guān)知識(shí)感興趣的朋友一起看看吧2021-09-09解決Eclipse發(fā)布到Tomcat丟失依賴jar包的問(wèn)題
這篇文章介紹了如何在Eclipse中配置部署裝配功能,以確保在將Web項(xiàng)目發(fā)布到Tomcat服務(wù)器時(shí)不會(huì)丟失任何依賴jar包,通過(guò)手動(dòng)配置或使用構(gòu)建工具腳本,可以自動(dòng)化這個(gè)過(guò)程,提高開發(fā)效率和應(yīng)用程序的穩(wěn)定性,感興趣的朋友跟隨小編一起看看吧2025-01-01SpringBoot Maven打包失敗報(bào):class lombok.javac.apt.Lombo
最新項(xiàng)目部署的時(shí)候,出現(xiàn)了一個(gè)maven打包失敗的問(wèn)題,報(bào):class lombok.javac.apt.LombokProcessor錯(cuò)誤,所以本文給大家介紹了如何解決SpringBoot Maven 打包失?。篶lass lombok.javac.apt.LombokProcessor 錯(cuò)誤,需要的朋友可以參考下2023-12-12三分鐘帶你掌握J(rèn)ava開發(fā)圖片驗(yàn)證碼功能方法
這篇文章主要來(lái)為大家詳細(xì)介紹Java實(shí)現(xiàn)開發(fā)圖片驗(yàn)證碼的具體方法,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以參考一下2023-02-02spring?cloud之eureka高可用集群和服務(wù)分區(qū)解析
這篇文章主要介紹了spring?cloud之eureka高可用集群和服務(wù)分區(qū)解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03