1.maven依賴
2. Quartz 核心概念
1.Job 表示一個(gè)工作搪锣,要執(zhí)行的具體內(nèi)容秋忙。
2.JobDetail 表示一個(gè)具體的可執(zhí)行的調(diào)度程序,Job 是這個(gè)可執(zhí)行程調(diào)度程序所要執(zhí)行的內(nèi)容构舟,另外 JobDetail 還包含了這個(gè)任務(wù)調(diào)度的方案和策略灰追。
3.Trigger 代表一個(gè)調(diào)度參數(shù)的配置,什么時(shí)候去調(diào)狗超。
4.Scheduler 代表一個(gè)調(diào)度容器弹澎,一個(gè)調(diào)度容器中可以注冊(cè)多個(gè) JobDetail 和 Trigger。當(dāng) Trigger 與 JobDetail 組合努咐,就可以被 Scheduler 容器調(diào)度了苦蒿。
jobDetail
Scheduler 通過jobDetail識(shí)別將要調(diào)用的job的類型,來調(diào)用job工作
我們傳給scheduler一個(gè)JobDetail實(shí)例渗稍,因?yàn)槲覀冊(cè)趧?chuàng)建JobDetail時(shí)佩迟,將要執(zhí)行的job的類名傳給了JobDetail,所以scheduler就知道了要執(zhí)行何種類型的job竿屹;每次當(dāng)scheduler執(zhí)行job時(shí)报强,在調(diào)用其execute(…)方法之前會(huì)創(chuàng)建該類的一個(gè)新的實(shí)例;執(zhí)行完畢拱燃,對(duì)該實(shí)例的引用就被丟棄了秉溉,實(shí)例會(huì)被垃圾回收;這種執(zhí)行策略帶來的一個(gè)后果是扼雏,job必須有一個(gè)無參的構(gòu)造函數(shù)(當(dāng)使用默認(rèn)的JobFactory時(shí))坚嗜;另一個(gè)后果是夯膀,在job類中诗充,不應(yīng)該定義有狀態(tài)的數(shù)據(jù)屬性,因?yàn)樵趈ob的多次執(zhí)行中诱建,這些屬性的值不會(huì)保留蝴蜓。
如果需要有狀態(tài)的數(shù)據(jù)屬性時(shí),需要用JobDataMap,這個(gè)就相當(dāng)于Map茎匠,可以將數(shù)據(jù)傳入job中
JobDataMap
JobDataMap中可以包含不限量的(序列化的)數(shù)據(jù)對(duì)象格仲,在job實(shí)例執(zhí)行的時(shí)候,可以使用其中的數(shù)據(jù)诵冒;JobDataMap是Java Map接口的一個(gè)實(shí)現(xiàn)凯肋,額外增加了一些便于存取基本類型的數(shù)據(jù)的方法。
將job加入到scheduler之前汽馋,在構(gòu)建JobDetail時(shí)侮东,可以將數(shù)據(jù)放入JobDataMap
Trigger 觸發(fā)器
Job中包含了任務(wù)執(zhí)行的邏輯,Scheduler負(fù)責(zé)掃描需要執(zhí)行的Job任務(wù)豹芯,trigger則定義job何時(shí)被執(zhí)行悄雅。
下面對(duì)觸發(fā)器的子類分別進(jìn)行講述,我們主要看Trigger的4個(gè)可用的派生類铁蹈,分別是:
org.quartz.SimpleTrigger
org.quartz.CronTrigger (常用)
org.quartz.DateIntervalTrigger
org.quartz.NthIncludedDayTrigger
SimpleTrigger
SimpleTrigger是一種設(shè)置和使用簡(jiǎn)單的觸發(fā)器宽闲,它是在指定日期/時(shí)間且可能需要重復(fù)執(zhí)行n次的時(shí)機(jī)下使用的。這種觸發(fā)器不適合每天定時(shí)執(zhí)行任務(wù)這種場(chǎng)景
它適合的任務(wù)類似于:9:00 開始握牧,每隔1小時(shí)容诬,每隔幾分鐘,每隔幾秒鐘執(zhí)行一次我碟。
CronTrigger
類似于SimpleTrigger放案,指定從某一個(gè)時(shí)間開始,以一定的時(shí)間間隔執(zhí)行的任務(wù)矫俺。 但是不同的是SimpleTrigger指定的時(shí)間間隔為毫秒吱殉,沒辦法指定每隔一個(gè)月執(zhí)行一次(每月的時(shí)間間隔不是固定值),而CalendarIntervalTrigger支持的間隔單位有秒厘托,分鐘友雳,小時(shí),天铅匹,月押赊,年,星期包斑。
它適合的任務(wù)類似于:9:00 開始執(zhí)行流礁,并且以后每周 9:00 執(zhí)行一次
Cron表達(dá)式的格式:秒 分 時(shí) 日 月 周 年(可選)
DailyTimeIntervalTrigger
指定每天的某個(gè)時(shí)間段內(nèi),以一定的時(shí)間間隔執(zhí)行任務(wù)罗丰。并且它可以支持指定星期神帅。
它適合的任務(wù)類似于:指定每天9:00 至 18:00 ,每隔70秒執(zhí)行一次萌抵,并且只要周一至周五執(zhí)行找御。
NthIncludedDayTrigger
NthIncludedDayTrigger適用于在每一個(gè)間隔類型(月或年等)的第N天觸發(fā)元镀。直接上代碼:
它適合的任務(wù)類似于:每個(gè)月的第10天中午12點(diǎn)整觸發(fā)
3.代碼實(shí)現(xiàn)
job 繼承QuartzJobBean實(shí)現(xiàn)executeInternal方法
4.misfired 策略
對(duì)于判定為misfired job,其實(shí)有很多條件霎桅,目前了解到的有:
1.到執(zhí)行時(shí)間時(shí)栖疑,上一個(gè)任務(wù)還未完成;
2.過期時(shí)間已超過設(shè)置的misfireThreshold滔驶;
3.線程池中已沒有空閑線程
4.線程池中雖有空閑線程遇革,但有優(yōu)先級(jí)更高的任務(wù)
5.服務(wù)器停止運(yùn)行
產(chǎn)生misfire后,會(huì)根據(jù)設(shè)置的misfire策略進(jìn)行任務(wù)的處理揭糕。
對(duì)CronTrigger來說澳淑,有三種misfire策略:
withMisfireHandlingInstructionDoNothing
不觸發(fā)立即執(zhí)行,等待下次Cron觸發(fā)頻率到達(dá)時(shí)刻開始按照Cron頻率依次執(zhí)行
withMisfireHandlingInstructionFireAndProceed
以錯(cuò)過的第一個(gè)頻率時(shí)間立刻開始執(zhí)行插佛,重做錯(cuò)過的所有頻率周期后杠巡,當(dāng)下一次觸發(fā)頻率發(fā)生時(shí)間大于當(dāng)前時(shí)間后,再按照正常的Cron頻率依次執(zhí)行
withMisfireHandlingInstructionIgnoreMisfires
以當(dāng)前時(shí)間為觸發(fā)頻率立刻觸發(fā)一次執(zhí)行雇寇,并發(fā)執(zhí)行一次做完氢拥,然后按照Cron頻率依次執(zhí)行
5.job、trigger锨侯、scheduler監(jiān)聽器
由于quartz沒有實(shí)現(xiàn)對(duì)任務(wù)執(zhí)行情況做記錄嫩海,所以主要用監(jiān)聽器來實(shí)現(xiàn)記錄任務(wù)的執(zhí)行情況,記錄任務(wù)是否被錯(cuò)過,任務(wù)具體什么時(shí)候被執(zhí)行,執(zhí)行過程中是否拋出異常等益眉。
監(jiān)聽器的執(zhí)行順序:
SimpleSchedulerListener.java
public class SimpleSchedulerListener extends SchedulerListenerSupport {
? ? private static Logger logger = LoggerFactory.getLogger(SimpleSchedulerListener.class);
? ? @Override
? ? public void jobScheduled(Trigger trigger) {
? ? ? ? String jobName = trigger.getJobKey().getName();
? ? ? ? logger.info(jobName + " has been scheduled");
? ? }
? ? @Override
? ? public void jobUnscheduled(TriggerKey triggerKey) {
? ? ? ? logger.info(triggerKey + " is being unscheduled");
? ? }
? ? @Override
? ? public void triggerFinalized(Trigger trigger) {
? ? ? ? logger.info("Trigger is finished for " + trigger.getJobKey().getName());
? ? }
? ? @Override
? ? public void triggerPaused(TriggerKey triggerKey) {
? ? ? ? logger.info(triggerKey + " is being paused");
? ? }
? ? @Override
? ? public void triggersPaused(String triggerGroup) {
? ? ? ? logger.info("trigger group "+triggerGroup + " is being paused");
? ? }
? ? @Override
? ? public void triggerResumed(TriggerKey triggerKey) {
? ? ? ? logger.info(triggerKey + " is being resumed");
? ? }
? ? @Override
? ? public void triggersResumed(String triggerGroup) {
? ? ? ? logger.info("trigger group "+triggerGroup + " is being resumed");
? ? }
? ? @Override
? ? public void jobAdded(JobDetail jobDetail) {
? ? ? ? logger.info(jobDetail.getKey()+" is added");
? ? }
? ? @Override
? ? public void jobDeleted(JobKey jobKey) {
? ? ? ? logger.info(jobKey+" is deleted");
? ? }
? ? @Override
? ? public void jobPaused(JobKey jobKey) {
? ? ? ? logger.info(jobKey+" is paused");
? ? }
? ? @Override
? ? public void jobsPaused(String jobGroup) {
? ? ? ? logger.info("job group "+jobGroup+" is paused");
? ? }
? ? @Override
? ? public void jobResumed(JobKey jobKey) {
? ? ? ? logger.info(jobKey+" is resumed");
? ? }
? ? @Override
? ? public void jobsResumed(String jobGroup) {
? ? ? ? logger.info("job group "+jobGroup+" is resumed");
? ? }
? ? @Override
? ? public void schedulerError(String msg, SchedulerException cause) {
? ? ? ? logger.error(msg, cause.getUnderlyingException());
? ? }
? ? @Override
? ? public void schedulerInStandbyMode() {
? ? ? ? logger.info("scheduler is in standby mode");
? ? }
? ? @Override
? ? public void schedulerStarted() {
? ? ? ? logger.info("scheduler has been started");
? ? }
? ? @Override
? ? public void schedulerStarting() {
? ? ? ? logger.info("scheduler is being started");
? ? }
? ? @Override
? ? public void schedulerShutdown() {
? ? ? ? logger.info("scheduler has been shutdown");
? ? }
? ? @Override
? ? public void schedulerShuttingdown() {
? ? ? ? logger.info("scheduler is being shutdown");
? ? }
? ? @Override
? ? public void schedulingDataCleared() {
? ? ? ? logger.info("scheduler has cleared all data");
? ? }
}
MonitorTriggerListener.java
public class MonitorTriggerListener extends TriggerListenerSupport {
? ? private static Logger log = LoggerFactory.getLogger(MonitorTriggerListener.class);
? ? @Override
? ? public String getName() {
? ? ? ? // TODO Auto-generated method stub
? ? ? ? return getClass().getSimpleName();
? ? }
? ? @Override
? ? public void triggerFired(Trigger trigger, JobExecutionContext jobExecutionContext) {
? ? ? ? //Trigger 被觸發(fā)了旭从,此時(shí)job上的execute()方法將要被執(zhí)行
? ? }
? ? @Override
? ? public boolean vetoJobExecution(Trigger trigger, JobExecutionContext jobExecutionContext) {
? ? ? ? //trigger被觸發(fā)后爪瓜,job將要被執(zhí)行時(shí)Scheduler調(diào)用該方法,如返回true則job此次將不被執(zhí)行
? ? ? ? return false;
? ? }
? ? @Override
? ? public void triggerMisfired(Trigger trigger) {
? ? ? ? log.info("當(dāng)前任務(wù)為錯(cuò)過任務(wù)" );
? ? ? ? trigger.getJobDataMap().put("isMisfire",true);
? ? }
? ? @Override
? ? public void triggerComplete(Trigger trigger, JobExecutionContext jobExecutionContext, Trigger.CompletedExecutionInstruction completedExecutionInstruction) {
? ? ? ? log.info("Trigger被觸發(fā)并且完成了job的執(zhí)行,此方法被調(diào)用"+trigger.getJobKey().getName());
? ? }
}
MonitorJobListener.java
public class MonitorJobListener extends JobListenerSupport {
? ? private final static Logger log = LoggerFactory.getLogger(MonitorJobListener.class);
? ? @Override
? ? public String getName() {
? ? ? ? return getClass().getSimpleName();
? ? }
? ? /**
? ? * (1)
? ? * 任務(wù)執(zhí)行之前執(zhí)行
? ? * Called by the Scheduler when a JobDetail is about to be executed (an associated Trigger has occurred).
? ? */
? ? @Override
? ? public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {
? ? ? ? log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"->開始任務(wù):" + jobExecutionContext.getJobDetail().getJobClass().getName());
? ? ? ? log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()));
? ? ? ? Long now = System.currentTimeMillis();
? ? ? ? Long jobfire = jobExecutionContext.getScheduledFireTime().getTime();
? ? ? ? log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(jobExecutionContext.getScheduledFireTime().getTime()));
? ? ? ? if (now - 10000L> jobfire){
? ? ? ? ? ? log.info("當(dāng)前任務(wù)為錯(cuò)過任務(wù)" );
? ? ? ? ? ? jobExecutionContext.getTrigger().getJobDataMap().put("isMisfire",true);
? ? ? ? }else {
? ? ? ? ? ? jobExecutionContext.getTrigger().getJobDataMap().put("isMisfire",false);
? ? ? ? }
? ? ? ? jobExecutionContext.getTrigger().getJobDataMap().put("now",now);
? ? }
? ? /**
? ? * (2)
? ? * 這個(gè)方法正常情況下不執(zhí)行,但是如果當(dāng)TriggerListener中的vetoJobExecution方法返回true時(shí),那么執(zhí)行這個(gè)方法.
? ? * 需要注意的是 如果方法(2)執(zhí)行 那么(1),(3)這個(gè)倆個(gè)方法不會(huì)執(zhí)行,因?yàn)槿蝿?wù)被終止了嘛.
? ? * Called by the Scheduler when a JobDetail was about to be executed (an associated Trigger has occurred),
? ? * but a TriggerListener vetoed it's execution.
? ? */
? ? @Override
? ? public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {
? ? ? ? log.info("========這個(gè)方法正常情況下不執(zhí)行,但是如果當(dāng)TriggerListener中的vetoJobExecution方法返回true時(shí),那么執(zhí)行這個(gè)方法.==========" + jobExecutionContext.getJobDetail().getJobClass().getName());
? ? }
? ? /**
? ? * (3)
? ? * 任務(wù)執(zhí)行完成后執(zhí)行,jobException如果它不為空則說明任務(wù)在執(zhí)行過程中出現(xiàn)了異常
? ? * Called by the Scheduler after a JobDetail has been executed, and be for the associated Trigger's triggered(xx) method has been called.
? ? */
? ? @Override
? ? public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) {
? ? ? ? //log.info("========任務(wù)執(zhí)行完成后執(zhí)行,jobException如果它不為空則說明任務(wù)在執(zhí)行過程中出現(xiàn)了異常==========" + getName());
? ? ? ? log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"->結(jié)束任務(wù)任務(wù):" + getName());
? ? ? ? Long now = (Long) jobExecutionContext.getTrigger().getJobDataMap().get("now");
? ? ? ? System.currentTimeMillis();
? ? ? ? JobBo jobService = SpringContextHelp.getBean(JobBo.class);
? ? ? ? Long jobfire = jobExecutionContext.getScheduledFireTime().getTime();
? ? ? ? String ip_address =? Constants.getApplicationValue("applicationContext","servlet.ip.address");
? ? ? ? Calendar cr = Calendar.getInstance();
? ? ? ? cr.setTimeInMillis(now);
? ? ? ? Date d1= cr.getTime();
? ? ? ? cr.setTimeInMillis(jobfire);
? ? ? ? Date d2 = cr.getTime();
? ? ? ? Boolean isMisfire = (Boolean) jobExecutionContext.getTrigger().getJobDataMap().get("isMisfire") == null? false:(Boolean) jobExecutionContext.getTrigger().getJobDataMap().get("isMisfire");
? ? ? ? try {
? ? ? ? ? ? QrtzTaskDetailVo vo = new? QrtzTaskDetailVo(jobExecutionContext.getScheduler().getSchedulerName(),
? ? ? ? ? ? ? ? ? ? jobExecutionContext.getJobDetail().getKey().getName(),
? ? ? ? ? ? ? ? ? ? jobExecutionContext.getJobDetail().getKey().getGroup(),
? ? ? ? ? ? ? ? ? ? jobExecutionContext.getTrigger().getKey().getName(),
? ? ? ? ? ? ? ? ? ? jobExecutionContext.getTrigger().getKey().getGroup(),
? ? ? ? ? ? ? ? ? ? d2,
? ? ? ? ? ? ? ? ? ? d1,isMisfire,ip_address);
? ? ? ? ? ? if (e != null){
? ? ? ? ? ? ? ? log.info(e.getMessage());
? ? ? ? ? ? ? vo.setError_detail(e.getMessage());
? ? ? ? ? ? ? vo.setFinish_status(2);
? ? ? ? ? ? }else {
? ? ? ? ? ? ? ? vo.setFinish_status(1);
? ? ? ? ? ? }
? ? ? ? ? ? jobService.saveQrtzTaskDetail(vo);
? ? ? ? } catch (SchedulerException s) {
? ? ? ? ? ? s.printStackTrace();
? ? ? ? }
? ? }
}
6.Quartz 集群
注意事項(xiàng)
時(shí)間同步問題
Quartz實(shí)際并不關(guān)心你是在相同還是不同的機(jī)器上運(yùn)行節(jié)點(diǎn)。當(dāng)集群放置在不同的機(jī)器上時(shí)血柳,稱之為水平集群。節(jié)點(diǎn)跑在同一臺(tái)機(jī)器上時(shí)生兆,稱之為垂直集群难捌。對(duì)于垂直集群,存在著單點(diǎn)故障的問題鸦难。這對(duì)高可用性的應(yīng)用來說是無法接受的根吁,因?yàn)橐坏C(jī)器崩潰了,所有的節(jié)點(diǎn)也就被終止了合蔽。對(duì)于水平集群击敌,存在著時(shí)間同步問題。
節(jié)點(diǎn)用時(shí)間戳來通知其他實(shí)例它自己的最后檢入時(shí)間辈末。假如節(jié)點(diǎn)的時(shí)鐘被設(shè)置為將來的時(shí)間愚争,那么運(yùn)行中的Scheduler將再也意識(shí)不到那個(gè)結(jié)點(diǎn)已經(jīng)宕掉了。另一方面挤聘,如果某個(gè)節(jié)點(diǎn)的時(shí)鐘被設(shè)置為過去的時(shí)間轰枝,也許另一節(jié)點(diǎn)就會(huì)認(rèn)定那個(gè)節(jié)點(diǎn)已宕掉并試圖接過它的Job重運(yùn)行。最簡(jiǎn)單的同步計(jì)算機(jī)時(shí)鐘的方式是使用某一個(gè)Internet時(shí)間服務(wù)器(Internet Time Server ITS)组去。
節(jié)點(diǎn)爭(zhēng)搶Job問題
因?yàn)镼uartz使用了一個(gè)隨機(jī)的負(fù)載均衡算法鞍陨, Job以隨機(jī)的方式由不同的實(shí)例執(zhí)行。Quartz官網(wǎng)上提到當(dāng)前从隆,還不存在一個(gè)方法來指派(釘住) 一個(gè) Job 到集群中特定的節(jié)點(diǎn)诚撵。