原文地址:http://blog.csdn.net/dailywater/article/details/51470779javascript
1、問題描述
使用Quartz配置定時任務,配置了超過10個定時任務,這些定時任務配置的觸發時間都是5分鐘執行一次,實際運行時,發現總有幾個定時任務不能執行到。css
2、示例程序
一、簡單介紹
採用spring+quartz整合方案實現定時任務,Quartz的SchedulerFactoryBean配置參數中不注入taskExecutor屬性,使用默認自帶的線程池。準備了15個定時任務,所有設置爲每隔10秒觸發一次,定時任務的實現邏輯是使用休眠8秒的方式模擬執行定時任務的時間耗費。java
二、配置文件信息以下(節選):spring
<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個 觸發器的配置 </list> </property> </bean> <bean id="testMethod1Trigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="jobDetail" ref="testMethod1" /> <!-- 指定Cron表達式:每10秒觸發一次 --> <property name="cronExpression" value="0/10 * * * * ?"/> </bean> <bean id="testMethod1" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="triggerService" /> <!-- 要執行的方法名稱 --> <property name="targetMethod" value="method1" /> </bean> // 如下省略14個定時任務的配置
三、Java定時任務類程序以下(節選)app
@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()); } }
實現邏輯很簡單,總共定義15個方法,方法內休眠6秒,同時每一個方法都使用一個成員變量記錄被調用的次數,並在該類的print()方法裏統一輸出全部方法調用次數的概況。框架
四、client啓動程序以下:oop
@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秒調用一次定時任務服務類的print()方法,輸出定時任務調用次數的統計值。源碼分析
五、運行結果
咱們讓這個demo程序跑了幾分鐘,控制檯輸出的取樣結果以下:測試
method1:25 method2:25 method3:25 method4:25 method5:12 method6:12 method7:12 method8:12 method9:12 method10:25 method11:25 method12:25 method13:25 method14:25 method15:25
六、結果分析
這次採樣的數據結果表示:15個任務中,有10個執行了25次,另外5個只執行了12次,執行的次數不同,說明在定時任務調度過程當中,有的任務會被遺漏不執行,目前的實驗結果可以重現上文描述的問題。ui
3、源碼分析
剛開始咱們對此也是感受到很疑惑,由於任務被漏執行時,沒有任何警告或報錯的日誌信息,這個問題若在實際生產中出現了,很難查明緣由。
咱們來看一下相關的源碼實現,但願能在源碼中發現一些有價值的信息:
1)SchedulerFactoryBean類的初始化操做
其中關於線程池屬性注入的相關代碼以下(省略了部分代碼):
/** * Load and/or apply Quartz properties to the given SchedulerFactory. * @param schedulerFactory the SchedulerFactory to initialize */ private void initSchedulerFactory(SchedulerFactory schedulerFactory) throws SchedulerException, IOException { if (!(schedulerFactory instanceof StdSchedulerFactory)) { if (this.configLocation != null || this.quartzProperties != null || this.taskExecutor != null || this.dataSource != null) { throw new IllegalArgumentException( "StdSchedulerFactory required for applying Quartz properties: " + schedulerFactory); } // Otherwise assume that no initialization is necessary... return; } // 省略其餘代碼... // 此爲須要關注的代碼 if (this.taskExecutor != null) { mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, LocalTaskExecutorThreadPool.class.getName()); } else { // Set necessary default properties here, as Quartz will not apply // its default configuration when explicitly given properties. mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, SimpleThreadPool.class.getName()); mergedProps.setProperty(PROP_THREAD_COUNT, Integer.toString(DEFAULT_THREAD_COUNT)); } // 省略其餘代碼... }
此代碼的邏輯是,若是taskExecutor屬性有注入值,就使用指定的線程池,通常Spring是會配置線程池的,線程池的參數能夠自行指定。若是taskExecutor未注入值,就使用org.quartz.simple.SimpleThreadPool線程池,DEFAULT_THREAD_COUNT的值爲10,即該線程池的大小爲10。
咱們如今演示的場景是未設置taskExecutor的,因此線程池是SimpleThreadPool的實例對象,池的大小爲10。
2)運行過程當中,定時任務的觸發過程
首先,要從線程池獲取可用資源,該實如今org.quartz.core.QuartzSchedulerThread線程類的run方法中,如代碼所示:
int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads(); //這個方法的實如今SimpleThreadPool類裏 public int blockForAvailableThreads() { synchronized(nextRunnableLock) { while((availWorkers.size() < 1 || handoffPending) && !isShutdown) { try { nextRunnableLock.wait(500); } catch (InterruptedException ignore) { } } return availWorkers.size(); } }
注意這個獲取線程池資源的方法是阻塞式的,若線程池資源不夠用,會一直等待直至獲取到可用的資源。這裏是產生等待的緣由。
而後咱們看一下定時任務容許被觸發的條件,實現的源碼仍是在
org.quartz.core.QuartzSchedulerThread線程類的run方法中:
try { triggers = qsRsrcs.getJobStore().acquireNextTriggers( now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow()); lastAcquireFailed = false; if (log.isDebugEnabled()) log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers"); } catch (JobPersistenceException jpe) { if(!lastAcquireFailed) { qs.notifySchedulerListenersError( "An error occurred while scanning for the next triggers to fire.",jpe); } lastAcquireFailed = true; continue; } catch (RuntimeException e) { if(!lastAcquireFailed) { getLog().error("quartzSchedulerThreadLoop: RuntimeException " +e.getMessage(), e); } lastAcquireFailed = true; continue; }
最關鍵的是acquireNextTriggers方法,這個方法是獲取全部可用的觸發器,定位到org.quartz.simpl.RAMJobStore實現類中,代碼以下:
/** * <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循環內調用的applyMisfire方法,實現以下:
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; }
以上源碼爲了節省篇幅有部分省略,有興趣的能夠自行閱讀完整代碼。
注意一下這裏返回爲false的判斷邏輯,這個方法返回爲false,表示acquireNextTriggers將再也不接收這個定時任務,而且沒有任何信息輸出,這樣該定時任務在觸發過程當中就被忽略不執行了。
順便留意一下misfireTime,它取當前的時間點,另外減少了5秒鐘(減少的時間參數能夠設置,默認是5秒),若是咱們把tnft.getTime()理解爲定時任務預先設定的執行時間,那麼」nextFireTime + misfireThreshold」咱們能夠理解爲任務執行的過時時間,misfireTime這個變量是用來跟nextFireTime比較的參數,若是nextFireTime大於misfireTime,即任務當前執行的時間點大於過時時間」nextFireTime + misfireThreshold」,表示任務已經超過了等待的限度,那麼這個任務就再也不被執行了。
簡單地說,就是一個定時任務通過獲取可用的線程池資源,到執行這段邏輯的時間,若是5秒內沒法完成的話, 這個任務就再也不執行了。
回想咱們的演示案例,定時任務是超過了10個,就確定存在線程池資源獲取等待的問題,而每一個定時任務的方法是休眠6秒鐘,又超過了5秒的限度,因此每次調度時,總有一些任務是被略過了的。
4、解決方案
通過以上分析,咱們已經瞭解到出現些問題的緣由,解決方案有兩種:
一、注入taskExecutor屬性,保證線程池資源是夠用的。
二、各個定時任務錯峯觸發。
演示案例的定時任務觸發時間均爲10秒一次,錯峯時間配置能夠參照素數原理,減少衝突可能性,好比配置時間爲5分鐘,7分鐘,11分鐘,13分鐘,17分鐘等,這樣高峯相遇的機率會低一些。
以上兩個方案可根據實際狀況挑選,也能夠組合使用。
5、總結
一、通過閱讀源碼分析,能夠了解到兩個關鍵點:線程池資源獲取等待定時任務過時做廢機制。
二、Quartz框架的定時任務執行是絕對時間觸發的,因此存在「過時不候」的現象。
三、在使用Quartzs框架時,必定要預先計算好triggers數量與線程池大小的匹配程度,資源必定要夠,或者任務執行密度不能太大,不然等到線程任務釋放完,trigger早已過時,就沒法按預期時間觸發了。
6、FAQ Q一、Quartz框架使用絕對時間觸發機制有什麼好處? A一、我我的以爲這種機制對運行環境是一種過載保護,若是任務負荷太重,已經來不及執行的,就適當放棄。如此一來,咱們使用就須要注意實際業務場景這種特性的存在,並經過適當增長線程資源,減少任務執行密度,任務錯峯觸發等方法來避免這種狀況發生。只是我的看法,僅做參考。