深刻Quartz任務調度器

#1.引言前端

Quartz是一個開源的任務調度框架。基於定時、按期的策略來執行任務是它的核心功能,好比2018年除夕夜晚上8點發紅包或者打開【木子道】公衆號,每隔10分鐘發1次紅包。Quartz有3個核心要素:調度器(Scheduler)、任務(Job)、觸發器(Trigger)。Quartz徹底使用Java開發,能夠集成到各類規模的應用程序中。它可以承載成千上萬的任務調度,而且支持集羣。它支持將數據存儲到數據庫中以實現持久化,並支持絕大多數的數據庫。它將任務與觸發設計爲鬆耦合,即一個任務能夠對應多個觸發器,這樣可以輕鬆構造出極爲複雜的觸發策略。java

#2.Quartz 核心API介紹(版本2.3.0)數據庫

Scheduler :與調度器交互的主要接口。express

Job:實現該接口組件能被調度程序執行的任務類。服務器

JobDetail:用於定義Job的實例。微信

Trigger:觸發器,定義一個Job如何被調度器執行。app

TriggerBuilder:觸發器建立器,用於建立觸發器Trigger實例。框架

JobBuilder :用於聲明一個任務實例,也能夠定義關於該任務的詳情好比任務名、組名等,這個聲明的實例將會做爲一個實際執行的任務。ide

Schedulersvn

void start() ;  開始啓動Scheduler的線程Trigger。

void shutdown() ;關閉Scheduler的Trigger,並清理與調度程序相關聯的全部資源。

boolean isShutdown(); Scheduler是否關閉

Date scheduleJob(JobDetail var1, Trigger var2) ;將該給定添加JobDetail到調度程序,並將給定Trigger與它關聯

void triggerJob(JobKey var1, JobDataMap var2) ;觸發標識JobDetail(當即執行)。

void pauseJob(JobKey var1) ; 暫停任務

void resumeJob(JobKey var1); 恢復任務

 boolean deleteJob(JobKey var1); 刪除任務
 ......

Scheduler由SchedulerFactory建立,並隨着shutdown方法的調用而終止。建立後它將可被用來添加、刪除或列出Job和Trigger,或執行一些調度相關的工做。Scheduler 建立完成後,處於「待命」模式,而且必須先start()調用其方法才能觸發任何Jobs只有經過start()方法啓動後它纔會真的工做。

Job

//這個接口由表明要執行的「工做」的類來實現。實例Job必須有一個public 無參數的構造函數。
void execute(JobExecutionContext var1) ;

當job的一個trigger被觸發後,execute()方法會被scheduler的一個工做線程調用;傳遞給execute()方法的JobExecutionContext對象中保存着該job運行時的一些信息 ,執行job的scheduler的引用,觸發job的trigger的引用,JobDetail對象引用,以及一些其它信息。

JobDetail

JobDataMap getJobDataMap(); //獲取實例成員數據
JobBuilder getJobBuilder();//得到JobBuilder對象

JobDetail對象是在將job加入scheduler時,由客戶端程序(你的程序)建立的。它包含job的各類屬性設置,以及用於存儲job實例狀態信息的JobDataMap。

Trigger

//獲取觸發器惟一標示,同分組key惟一,key= key + 組名稱,默認組名稱DEFAULT。
TriggerKey getKey(); 
//獲取任務惟一標示,
JobKey getJobKey();
//獲取實例數據。
JobDataMap getJobDataMap();
//觸發優先級,默認是5,同時觸發纔會比較優先級。
int getPriority();

Trigger用於觸發Job的執行。當你準備調度一個job時,你建立一個Trigger的實例,而後設置調度相關的屬性。Trigger也有一個相關聯的JobDataMap,用於給Job傳遞一些觸發相關的參數。Quartz自帶了各類不一樣類型的Trigger,最經常使用的主要是SimpleTrigger和CronTrigger。

CronTrigger CronTrigger一般比SimpleTrigger更有用,若是您須要一個基於相似日曆的概念重複出現的工做調度計劃,而不是SimpleTrigger的精確指定時間間隔。

使用CronTrigger,您能夠在每週一上午九點打開個人微信公衆號【木子道】,查閱相關技術分享文章。還能夠每週五晚8點給我回復。

即便如此,就像SimpleTrigger同樣,CronTrigger有一個startTime,指定調度什麼時候生效,還有一個(可選)endTime,指定什麼時候應該中止調度。

Cron表達式 Cron-Expressions用於配置CronTrigger的實例。Cron-Expressions是由七個子表達式組成的字符串,它們描述了計劃的各個細節。這些子表達式用空格分隔,表示: 秒 分鐘 小時 日 月 星期 年份(可選)
一個完整的cron-expression的例子是字符串「0 0 0 1 * ?「 - 這意味着」每一個每個月1日00:00:00 發佈統計上個月數據「。

單獨的子表達式能夠包含範圍和/或列表。例如,能夠用「MON-FRI」,「MON,WED,FRI」,甚至「MON-WED,SAT」代替先前(以「WED」)爲例的星期幾字段。

通配符(「字符」)能夠用來表示該字段的「每一個」可能的值。所以,上例中「月」字段中的字符只是「每個月」。所以,「星期幾」字段中的「*」顯然意味着「每週的每一天」。

全部的字段都有一組能夠指定的有效值。這些值應該至關明顯 - 例如秒和分鐘的數字0到59,以及數小時的值0到23。月的日期能夠是1-31的任何值,可是您須要當心給定的月份有多少天!能夠將月份指定爲0到11之間的值,或者使用字符串JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV和DEC。能夠將星期幾指定爲1到7之間的值(1 =星期日),或者使用字符串SUN,MON,TUE,WED,THU,FRI和SAT。

「/」字符可用於指定增量值。例如,若是在「分鐘」字段中輸入「0/15」,則表示「每分鐘15分鐘,從零開始」。若是在「分鐘」字段中使用「3/20」,則意味着「從第三分鐘開始的每20分鐘一小時」 - 或者換句話說,就是在分鐘中指定「3,23,43」領域。請注意「 / 35」並不意味着「每35分鐘」 的微妙含義,即「每分鐘35分鐘,從零開始」,換句話說就是指定「0,35」。

'?' 字符被容許用於日期和星期幾字段。它用來指定「沒有具體的價值」。當你須要在兩個字段中的一個字段中指定某些內容時,這是很是有用的,而不是其餘的。請參閱下面的示例(和CronTrigger JavaDoc)進行說明。

月份和星期幾字段容許使用「L」字符。這個角色對於「最後」來講是短暫的,可是在兩個領域中的每個都有不一樣的含義。例如,月份字段中的值「L」意味着「月份的最後一天」,即非閏年的2月28日的1月31日。若是單獨使用在星期幾字段中,則僅表示「7」或「SAT」。可是,若是在星期幾字段中使用另外一個值,則表示「本月的最後一個xxx日」,例如「6L」或「FRIL」都表示「本月的最後一個星期五」。您還能夠指定月份的最後一天的偏移量,例如「L-3」,表示日曆月份的倒數第三天。當使用「L」選項時,不要指定列表或值的範圍,由於您會獲得使人困惑/意外的結果。

「W」用於指定與指定日期最近的星期幾(星期一至星期五)。例如,若是您要指定「15W」做爲月份日期字段的值,則其含義是:「最近的星期幾到本月15日」。

「#」用於指定該月的第n個「XXX」工做日。例如,星期幾字段中的「6#3」或「FRI#3」的值表示「月的第三個星期五」。

示例Cron表達式

CronTrigger示例1 -建立一個觸發器的表達式,每5分鐘查閱【木子道】公衆號 「0 0/5 * * *?」

CronTrigger示例2 - 一個表達式,用於建立在分鐘後10秒(即上午10:00:10,上午10:05:10等)每5分鐘觸發一次的觸發器。 「10 0/5 * * *?」

CronTrigger示例3 - 一個表達式,用於建立在每週三和週五的10:30,11:30,12:30和13:30發生的觸發器。 「0 30 10-13?* WED,FRI「

CronTrigger示例4 - 一個表達式,用於建立一個觸發器,在每月的第5天和第20天的上午8點到上午10點之間每隔半小時觸發一次。請注意,觸發器不會在上午10點,僅在8點,8點,9點和9點30分 「0 0/30 8-9 5,20 *?」

請注意,一些調度要求過於複雜,沒法用一個觸發器來表示 - 例如「上午9點至上午10點之間每5分鐘一次,下午1點至10點之間每20分鐘一次」。在這種狀況下解決方案是簡單地建立兩個觸發器,並註冊他們兩個運行相同的工做。

JobStore JobStore負責跟蹤全部給調度器的「工做數據」:做業,觸發器,日曆等。爲您的Quartz調度器實例選擇合適的JobStore是很是重要的一步。幸運的是,一旦你瞭解它們之間的差別,選擇應該是一個很是簡單的選擇。您能夠在您提供給SchedulerFactory的屬性文件(或對象)中聲明您的調度程序應使用哪一個JobStore(以及它的配置設置),以便生成調度程序實例。

切勿在代碼中直接使用JobStore實例。出於某種緣由,許多人試圖這樣作。JobStore用於石英自己的幕後使用。你必須告訴Quartz(經過配置)使用哪一個JobStore,可是你只能在你的代碼中使用Scheduler接口。

RAMJobStore RAMJobStore是最簡單的JobStore,它也是性能最高的(CPU時間)。RAMJobStore以明顯的方式獲得它的名字:它將全部的數據保存在RAM中。這就是爲何它閃電般快速,也是爲何配置如此簡單。缺點是,當你的應用程序結束(或崩潰)時,全部的調度信息都將丟失 - 這意味着RAMJobStore沒法遵照做業和觸發器的「非易失性」設置。對於某些應用程序來講,這是能夠接受的 - 甚至是所需的行爲,可是對於其餘應用程序來講,這多是災難性的。

JDBCJobStore JDBCJobStore也被恰當地命名 - 它經過JDBC將其全部數據保存在數據庫中。所以,配置比RAMJobStore要複雜一些,並且速度也不是那麼快。可是,性能退化不是很是糟糕,特別是若是您使用主鍵上的索引構建數據庫表時。在至關現代的機器上有一個體面的局域網(在調度器和數據庫之間),檢索和更新觸發器的時間一般小於10毫秒。

JDBCJobStore幾乎能夠與任何數據庫一塊兒使用,它已經被普遍地用於Oracle,PostgreSQL,MySQL,MS SQLServer,HSQLDB和DB2。要使用JDBCJobStore,必須先爲Quartz建立一組數據庫表來使用。您能夠在Quartz發行版的「docs / dbTables」目錄中找到建立表的SQL腳本。若是您的數據庫類型沒有腳本,只需查看其中一個腳本,而後以任何須要的方式修改它。有一點須要注意的是,在這些腳本中,全部的表都之前綴「QRTZ_」開頭(例如表「QRTZ_TRIGGERS」和「QRTZ_JOB_DETAIL」)。這個前綴實際上能夠是任何你想要的,只要你通知JDBCJobStore什麼前綴(在你的Quartz屬性中)。使用不一樣的前綴對於建立多個表集合,對於多個調度器實例,

一旦建立了表,在配置和啓動JDBCJobStore以前,還有一個重要的決定。您須要肯定您的應用程序須要什麼類型的事務。若是您不須要將調度命令(如添加和刪除觸發器)與其餘事務綁定,那麼您可讓Quartz使用JobStoreTX做爲JobStore(這是最多見的選擇)來管理事務。

若是您須要Quartz與其餘事務(即在J2EE應用程序服務器中)一塊兒工做,那麼您應該使用JobStoreCMT--在這種狀況下,Quartz將讓應用程序服務器容器管理事務。

最後一塊難題是設置一個JDBCJobStore能夠鏈接到數據庫的DataSource。DataSources是使用幾種不一樣的方法之一在你的Quartz屬性中定義的。一種方法是讓Quartz建立和管理DataSource自己 - 經過提供數據庫的全部鏈接信息。另外一種方法是讓Quartz使用由Quartz運行的應用程序服務器管理的DataSource - 經過向JDBCJobStore提供DataSource的JNDI名稱。

TerracottaJobStore TerracottaJobStore在不使用數據庫的狀況下提供了擴展性和健壯性的手段。這意味着您的數據庫能夠從Quartz中免費下載,而且能夠將其全部資源保存到您的應用程序的其他部分。

TerracottaJobStore能夠運行羣集或非羣集,而且在任何狀況下爲應用程序從新啓動之間的持續工做數據提供存儲介質,由於數據存儲在Terracotta服務器中。它的性能比經過JDBCJobStore使用數據庫要好得多(大約好一個數量級),可是比RAMJobStore慢得多。

#3.Quartz 的配置與使用

//配置SchedulerFactory
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);
        //quartz參數
        Properties prop = new Properties();
        prop.put("org.quartz.scheduler.instanceName", "AstaliScheduler");
        prop.put("org.quartz.scheduler.instanceId", "AUTO");
        //線程池配置
        prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        prop.put("org.quartz.threadPool.threadCount", "20");
        prop.put("org.quartz.threadPool.threadPriority", "5");
        //JobStore配置
       //SQL腳本。http://svn.terracotta.org/svn/quartz/tags/quartz-2.1.3/docs/dbTables/ 
        prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
        //集羣配置
        prop.put("org.quartz.jobStore.isClustered", "true"); //開啓集羣
        prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
        prop.put("org.quartz.jobStore.misfireThreshold", "12000");
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");  //表頭
        factory.setQuartzProperties(prop);
        factory.setSchedulerName("AstaliScheduler");
        //延時啓動
        factory.setStartupDelay(30);
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");
        //可選,QuartzScheduler 啓動時更新己存在的Job,
        //這樣就不用每次修改targetObject後刪除qrtz_job_details表對應記錄了
        factory.setOverwriteExistingJobs(true);
        //設置自動啓動,默認爲true
        factory.setAutoStartup(true);
        return factory;
    }
}
//構建Job實例   建立任務
//ScheduleJob extents QuartzJobBean,QuartzJobBean implements Job
 JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class)
                    .withIdentity(getJobKey(scheduleJobEntity.getJobId()))
                    .build();

//構建cron
  CronScheduleBuilder scheduleBuilde = CronScheduleBuilder
                                     .cronSchedule(scheduleJob.getCronExpression())
                                     .withMisfireHandlingInstructionDoNothing();
 //根據cron,構建一個CronTrigger
 CronTrigger trigger = TriggerBuilder.newTrigger()
                          .withIdentity(getTriggerKey(scheduleJob.getJobId()))
                          .withSchedule(scheduleBuilder)
                          .build();
//調度器的任務類與觸發器關聯
 scheduler.scheduleJob(jobDetail, trigger);
//當即執行任務
  JobDataMap dataMap = new JobDataMap();
  dataMap.put(ScheduleJobEntity.JOB_PARAM_KEY, new 
Gson().toJson(scheduleJob));
 scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap);
//暫定任務
scheduler.pauseJob(getJobKey(jobId));
//恢復任務
 scheduler.resumeJob(getJobKey(jobId));
//刪除任務
 scheduler.deleteJob(getJobKey(jobId));
/**
     * 獲取觸發器key
     */
    private static TriggerKey getTriggerKey(Long jobId) {
        return TriggerKey.triggerKey(JOB_NAME + jobId);
    }
     /**
     * 獲取jobKey
     */
    private static JobKey getJobKey(Long jobId) {
        return JobKey.jobKey(JOB_NAME + jobId);
    }
    /**
     * 獲取表達式觸發器
     */
    public static CronTrigger getCronTrigger(Scheduler scheduler, Long jobId) {
        try {
            return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId));
        } catch (SchedulerException e) {
            throw new Exception("getCronTrigger異常,請檢查qrtz開頭的表,是否有髒數據", e);
        }
    }
//上篇文章對newSingleThreadExecutor有說明
ExecutorService service = Executors.newSingleThreadExecutor();    
//執行定時任務實現Runnable並重寫run方法
ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(),               
                              scheduleJob.getMethodName(),     
                              scheduleJob.getParams());
//提交任務並有結果
Future<?> future = service.submit(task); 
future.get();

	@Override
	public void run() {
		try {
   //利用反射執行方法
			ReflectionUtils.makeAccessible(method);
			if(StringUtils.isNotBlank(params)){
				method.invoke(target, params);
			}else{
				method.invoke(target);
			}
		}catch (Exception e) {
			throw new Exception("執行定時任務失敗", e);
		}
	}
創建一個觸發器,每隔上午8點到下午5點,每隔一分鐘一次:
  trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(cronSchedule("0 0/2 8-17 * * ?"))
    .forJob("myJob", "group1")
    .build();
創建一個觸發器,天天在上午10:42開槍:
  trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(dailyAtHourAndMinute(10, 42))
    .forJob(myJobKey)
    .build();

效果圖 前端效果圖 須要源碼的請關注公衆號【木子道】▼長按如下二維碼便可關注▼公衆號二維碼2018年請與我一塊兒打怪升級

相關文章
相關標籤/搜索