Quartz框架多个trigger任务执行出现漏执行的问题分析--转
原文地址:http://blog.csdn.net/dailywater/article/details/51470779
一、問題描述?
使用Quartz配置定時任務(wù),配置了超過10個定時任務(wù),這些定時任務(wù)配置的觸發(fā)時間都是5分鐘執(zhí)行一次,實際運(yùn)行時,發(fā)現(xiàn)總有幾個定時任務(wù)不能執(zhí)行到。
二、示例程序?
1、簡單介紹?
采用spring+quartz整合方案實現(xiàn)定時任務(wù),Quartz的SchedulerFactoryBean配置參數(shù)中不注入taskExecutor屬性,使用默認(rèn)自帶的線程池。準(zhǔn)備了15個定時任務(wù),全部設(shè)置為每隔10秒觸發(fā)一次,定時任務(wù)的實現(xiàn)邏輯是使用休眠8秒的方式模擬執(zhí)行定時任務(wù)的時間耗費(fèi)。
2、配置文件信息如下(節(jié)選):
<bean id="startQuertz" lazy-init="false" autowire="no" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="testMethod1Trigger"/> <ref bean="testMethod2Trigger"/> // 以下省略13個 觸發(fā)器的配置 </list> </property> </bean> <bean id="testMethod1Trigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="jobDetail" ref="testMethod1" /> <!-- 指定Cron表達(dá)式:每10秒觸發(fā)一次 --> <property name="cronExpression" value="0/10 * * * * ?"/> </bean> <bean id="testMethod1" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="triggerService" /> <!-- 要執(zhí)行的方法名稱 --> <property name="targetMethod" value="method1" /> </bean> // 以下省略14個定時任務(wù)的配置3、Java定時任務(wù)類程序如下(節(jié)選)
@Service("triggerService") public class TriggerService {private int cnt1; public void method1() { try { Thread.sleep(8000); } catch (InterruptedException e) { } cnt1++; } public void print() { StringBuffer sb = new StringBuffer(); sb.append("\nmethod1:" + cnt1); sb.append("\nmethod2:" + cnt2); sb.append("\nmethod3:" + cnt3); sb.append("\nmethod4:" + cnt4); sb.append("\nmethod5:" + cnt5); sb.append("\nmethod6:" + cnt6); sb.append("\nmethod7:" + cnt7); sb.append("\nmethod8:" + cnt8); sb.append("\nmethod9:" + cnt9); sb.append("\nmethod10:" + cnt10); sb.append("\nmethod11:" + cnt11); sb.append("\nmethod12:" + cnt12); sb.append("\nmethod13:" + cnt13); sb.append("\nmethod14:" + cnt14); sb.append("\nmethod15:" + cnt15); System.out.println(sb.toString()); } }實現(xiàn)邏輯很簡單,總共定義15個方法,方法內(nèi)休眠6秒,同時每個方法都使用一個成員變量記錄被調(diào)用的次數(shù),并在該類的print()方法里統(tǒng)一輸出所有方法調(diào)用次數(shù)的概況。
4、client啟動程序如下:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:applicationContext.xml") public class TriggerServiceTest extends TestCase { @Autowired private TriggerService triggerService; @Test public void testService() { try { while (true) { Thread.sleep(11000); triggerService.print(); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }一個簡單的單元測試用例,每隔11秒調(diào)用一次定時任務(wù)服務(wù)類的print()方法,輸出定時任務(wù)調(diào)用次數(shù)的統(tǒng)計值。
5、運(yùn)行結(jié)果?
我們讓這個demo程序跑了幾分鐘,控制臺輸出的取樣結(jié)果如下:
6、結(jié)果分析?
此次采樣的數(shù)據(jù)結(jié)果表示:15個任務(wù)中,有10個執(zhí)行了25次,另外5個只執(zhí)行了12次,執(zhí)行的次數(shù)不一樣,說明在定時任務(wù)調(diào)度過程中,有的任務(wù)會被遺漏不執(zhí)行,目前的實驗結(jié)果能夠重現(xiàn)上文描述的問題。
三、源碼分析?
剛開始我們對此也是感覺到很疑惑,因為任務(wù)被漏執(zhí)行時,沒有任何警告或報錯的日志信息,這個問題若在實際生產(chǎn)中出現(xiàn)了,很難查明原因。?
我們來看一下相關(guān)的源碼實現(xiàn),希望能在源碼中發(fā)現(xiàn)一些有價值的信息:?
1)SchedulerFactoryBean類的初始化操作?
其中關(guān)于線程池屬性注入的相關(guān)代碼如下(省略了部分代碼):
此代碼的邏輯是,如果taskExecutor屬性有注入值,就使用指定的線程池,一般Spring是會配置線程池的,線程池的參數(shù)可以自行指定。如果taskExecutor未注入值,就使用org.quartz.simple.SimpleThreadPool線程池,DEFAULT_THREAD_COUNT的值為10,即該線程池的大小為10。?
我們現(xiàn)在演示的場景是未設(shè)置taskExecutor的,所以線程池是SimpleThreadPool的實例對象,池的大小為10。
2)運(yùn)行過程中,定時任務(wù)的觸發(fā)過程?
首先,要從線程池獲取可用資源,該實現(xiàn)在org.quartz.core.QuartzSchedulerThread線程類的run方法中,如代碼所示:
注意這個獲取線程池資源的方法是阻塞式的,若線程池資源不夠用,會一直等待直至獲取到可用的資源。這里是產(chǎn)生等待的原因。
然后我們看一下定時任務(wù)允許被觸發(fā)的條件,實現(xiàn)的源碼還是在?
org.quartz.core.QuartzSchedulerThread線程類的run方法中:
最關(guān)鍵的是acquireNextTriggers方法,這個方法是獲取所有可用的觸發(fā)器,定位到org.quartz.simpl.RAMJobStore實現(xiàn)類中,代碼如下:
/*** <p>* Get a handle to the next trigger to be fired, and mark it as 'reserved'* by the calling scheduler.* </p>** @see #releaseAcquiredTrigger(OperableTrigger)*/ public List<OperableTrigger> acquireNextTriggers(long noLaterThan, int maxCount, long timeWindow) { synchronized (lock) { List<OperableTrigger> result = new ArrayList<OperableTrigger>(); Set<JobKey> acquiredJobKeysForNoConcurrentExec = new HashSet<JobKey>(); Set<TriggerWrapper> excludedTriggers = new HashSet<TriggerWrapper>(); long firstAcquiredTriggerFireTime = 0; // return empty list if store has no triggers. if (timeTriggers.size() == 0) return result; while (true) { TriggerWrapper tw; try { tw = timeTriggers.first(); if (tw == null) break; timeTriggers.remove(tw); } catch (java.util.NoSuchElementException nsee) { break; } if (tw.trigger.getNextFireTime() == null) { continue; } if (applyMisfire(tw)) { if (tw.trigger.getNextFireTime() != null) { timeTriggers.add(tw); } continue; } if (tw.getTrigger().getNextFireTime().getTime() > noLaterThan + timeWindow) { timeTriggers.add(tw); break; } // 省略部分代碼... if (result.size() == maxCount) break; } // If we did excluded triggers to prevent ACQUIRE state due to DisallowConcurrentExecution, we need to add them back to store. if (excludedTriggers.size() > 0) timeTriggers.addAll(excludedTriggers); return result; } }請注意一下while循環(huán)內(nèi)調(diào)用的applyMisfire方法,實現(xiàn)如下:
protected boolean applyMisfire(TriggerWrapper tw) {long misfireTime = System.currentTimeMillis();if (getMisfireThreshold() > 0) {misfireTime -= getMisfireThreshold();}Date tnft = tw.trigger.getNextFireTime();if (tnft == null || tnft.getTime() > misfireTime|| tw.trigger.getMisfireInstruction() == Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY) {return false;}// 省略其他代碼... return true; }以上源碼為了節(jié)省篇幅有部分省略,有興趣的可以自行閱讀完整代碼。
注意一下這里返回為false的判斷邏輯,這個方法返回為false,表示acquireNextTriggers將不再接收這個定時任務(wù),并且沒有任何信息輸出,這樣該定時任務(wù)在觸發(fā)過程中就被忽略不執(zhí)行了。
順便留意一下misfireTime,它取當(dāng)前的時間點(diǎn),另外減小了5秒鐘(減小的時間參數(shù)可以設(shè)置,默認(rèn)是5秒),如果我們把tnft.getTime()理解為定時任務(wù)預(yù)先設(shè)定的執(zhí)行時間,那么”nextFireTime + misfireThreshold”我們可以理解為任務(wù)執(zhí)行的過期時間,misfireTime這個變量是用來跟nextFireTime比較的參數(shù),如果nextFireTime大于misfireTime,即任務(wù)當(dāng)前執(zhí)行的時間點(diǎn)大于過期時間”nextFireTime + misfireThreshold”,表示任務(wù)已經(jīng)超過了等待的限度,那么這個任務(wù)就不再被執(zhí)行了。?
簡單地說,就是一個定時任務(wù)經(jīng)過獲取可用的線程池資源,到執(zhí)行這段邏輯的時間,如果5秒內(nèi)無法完成的話, 這個任務(wù)就不再執(zhí)行了。
回想我們的演示案例,定時任務(wù)是超過了10個,就肯定存在線程池資源獲取等待的問題,而每個定時任務(wù)的方法是休眠6秒鐘,又超過了5秒的限度,所以每次調(diào)度時,總有一些任務(wù)是被略過了的。
四、解決方案?
經(jīng)過以上分析,我們已經(jīng)了解到出現(xiàn)些問題的原因,解決方案有兩種:?
1、注入taskExecutor屬性,保證線程池資源是夠用的。?
2、各個定時任務(wù)錯峰觸發(fā)。?
演示案例的定時任務(wù)觸發(fā)時間均為10秒一次,錯峰時間配置可以參照素數(shù)原理,減小沖突可能性,比如配置時間為5分鐘,7分鐘,11分鐘,13分鐘,17分鐘等,這樣高峰相遇的概率會低一些。?
以上兩個方案可根據(jù)實際情況挑選,也可以組合使用。
五、總結(jié)?
1、經(jīng)過閱讀源碼分析,可以了解到兩個關(guān)鍵點(diǎn):線程池資源獲取等待定時任務(wù)過期作廢機(jī)制。?
2、Quartz框架的定時任務(wù)執(zhí)行是絕對時間觸發(fā)的,所以存在“過期不候”的現(xiàn)象。?
3、在使用Quartzs框架時,一定要預(yù)先計算好triggers數(shù)量與線程池大小的匹配程度,資源一定要夠,或者任務(wù)執(zhí)行密度不能太大,否則等到線程任務(wù)釋放完,trigger早已過期,就無法按預(yù)期時間觸發(fā)了。
六、FAQ?
Q1、Quartz框架使用絕對時間觸發(fā)機(jī)制有什么好處??
A1、我個人覺得這種機(jī)制對運(yùn)行環(huán)境是一種過載保護(hù),如果任務(wù)負(fù)荷過重,已經(jīng)來不及執(zhí)行的,就適當(dāng)放棄。如此一來,我們使用就需要注意實際業(yè)務(wù)場景這種特性的存在,并通過適當(dāng)增加線程資源,減小任務(wù)執(zhí)行密度,任務(wù)錯峰觸發(fā)等方法來避免這種情況發(fā)生。只是個人見解,僅作參考。
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/7017210.html
總結(jié)
以上是生活随笔為你收集整理的Quartz框架多个trigger任务执行出现漏执行的问题分析--转的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BigDecimal相除异常
- 下一篇: Kafka剖析(一):Kafka背景及架