轉自:http://www.cnblogs.com/bingoidea/archive/2009/08/05/1539656.htmlhtml
上一篇:定時器的實現、Java定時器Timer和Quartz介紹與spring中定時器的配置http://blog.csdn.NET/sup_heaven/article/details/37738255java
概述mysql
各類企業應用幾乎都會碰到任務調度的需求,就拿論壇來講:每隔半個小時生成精華文章的RSS文件,天天凌晨統計論壇用戶的積分排名,每隔30分鐘執行鎖定用戶解鎖任務。linux
對於一個典型的MIS系統來講,在每個月1號凌晨統計上個月各部門的業務數據生成月報表,每半個小時查詢用戶是否已經有快到期的待處理業務……,這樣的例子俯拾皆是,不勝枚舉。web
任務調度自己涉及到多線程併發、運行時間規則制定和解析、場景保持與恢復、線程池維護等諸多方面的工做。若是直接使用自定義線程這種刀耕火種的原始辦法,開發任務調度程序是一項頗具挑戰性的工做。Java開源的好處就是:領域問題都能找到現成的解決方案。spring
OpenSymphony所提供的Quartz自2001年發佈版本以來已經被衆多項目做爲任務調度的解決方案,Quartz在提供巨大靈活性的同時並未犧牲其簡單性,它所提供的強大功能使你能夠應付絕大多數的調度需求。sql
Quartz 在開源任務調度框架中的翹首,它提供了強大任務調度機制,難能難得的是它同時保持了使用的簡單性。Quartz 容許開發人員靈活地定義觸發器的調度時間表,並能夠對觸發器和任務進行關聯映射。數據庫
此外,Quartz提供了調度運行環境的持久化機制,能夠保存並恢復調度現場,即便系統因故障關閉,任務調度現場數據並不會丟失。此外,Quartz還提供了組件式的偵聽器、各類插件、線程池等功能。服務器
瞭解Quartz體系結構多線程
Quartz對任務調度的領域問題進行了高度的抽象,提出了調度器、任務和觸發器這3個核心的概念,並在org.quartz經過接口和類對重要的這些核心概念進行描述:
●Job:是一個接口,只有一個方法void execute(JobExecutionContext context),開發者實現該接口定義運行任務,JobExecutionContext類提供了調度上下文的各類信息。Job運行時的信息保存在JobExecutionContext實例中;
●JobDetail:Quartz在每次執行Job時,都從新建立一個Job實例,因此它不直接接受一個Job的實例,相反它接收一個Job實現類,以便運行時經過newInstance()的反射機制實例化Job。所以須要經過一個類來描述Job的實現類及其它相關的靜態信息,如Job名字、描述、關聯監聽器等信息,JobDetail承擔了這一角色。
經過該類的構造函數能夠更具體地瞭解它的功用:JobDetail(java.lang.String name, java.lang.String group, java.lang.Class jobClass),該構造函數要求指定Job的實現類,以及任務在Scheduler中的組名和Job名稱;
●Trigger:是一個類,描述觸發Job執行的時間觸發規則。主要有SimpleTrigger和CronTrigger這兩個子類。當僅需觸發一次或者以固定時間間隔週期執行,SimpleTrigger是最適合的選擇;而CronTrigger則能夠經過Cron表達式定義出各類複雜時間規則的調度方案:如每早晨9:00執行,周1、周3、週五下午5:00執行等;
●Calendar:org.quartz.Calendar和java.util.Calendar不一樣,它是一些日曆特定時間點的集合(能夠簡單地將org.quartz.Calendar看做java.util.Calendar的集合——java.util.Calendar表明一個日曆時間點,無特殊說明後面的Calendar即指org.quartz.Calendar)。一個Trigger能夠和多個Calendar關聯,以便排除或包含某些時間點。
假設,咱們安排每週星期一早上10:00執行任務,可是若是碰到法定的節日,任務則不執行,這時就須要在Trigger觸發機制的基礎上使用Calendar進行定點排除。針對不一樣時間段類型,Quartz在org.quartz.impl.calendar包下提供了若干個Calendar的實現類,如AnnualCalendar、MonthlyCalendar、WeeklyCalendar分別針對每一年、每個月和每週進行定義;
●Scheduler:表明一個Quartz的獨立運行容器,Trigger和JobDetail能夠註冊到Scheduler中,二者在Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查找定位容器中某一對象的依據,Trigger的組及名稱必須惟一,JobDetail的組和名稱也必須惟一(但能夠和Trigger的組和名稱相同,由於它們是不一樣類型的)。Scheduler定義了多個接口方法,容許外部經過組及名稱訪問和控制容器中Trigger和JobDetail。
Scheduler能夠將Trigger綁定到某一JobDetail中,這樣當Trigger觸發時,對應的Job就被執行。一個Job能夠對應多個Trigger,但一個Trigger只能對應一個Job。能夠經過SchedulerFactory建立一個Scheduler實例。Scheduler擁有一個SchedulerContext,它相似於ServletContext,保存着Scheduler上下文信息,Job和Trigger均可以訪問SchedulerContext內的信息。SchedulerContext內部經過一個Map,以鍵值對的方式維護這些上下文數據,SchedulerContext爲保存和獲取數據提供了多個put()和getXxx()的方法。能夠經過Scheduler# getContext()獲取對應的SchedulerContext實例;
●ThreadPool:Scheduler使用一個線程池做爲任務運行的基礎設施,任務經過共享線程池中的線程提升運行效率。
Job有一個StatefulJob子接口,表明有狀態的任務,該接口是一個沒有方法的標籤接口,其目的是讓Quartz知道任務的類型,以便採用不一樣的執行方案。無狀態任務在執行時擁有本身的JobDataMap拷貝,對JobDataMap的更改不會影響下次的執行。而有狀態任務共享共享同一個JobDataMap實例,每次任務執行對JobDataMap所作的更改會保存下來,後面的執行能夠看到這個更改,也即每次執行任務後都會對後面的執行發生影響。
正由於這個緣由,無狀態的Job能夠併發執行,而有狀態的StatefulJob不能併發執行,這意味着若是前次的StatefulJob尚未執行完畢,下一次的任務將阻塞等待,直到前次任務執行完畢。有狀態任務比無狀態任務須要考慮更多的因素,程序每每擁有更高的複雜度,所以除非必要,應該儘可能使用無狀態的Job。
若是Quartz使用了數據庫持久化任務調度信息,無狀態的JobDataMap僅會在Scheduler註冊任務時保持一次,而有狀態任務對應的JobDataMap在每次執行任務後都會進行保存。
Trigger自身也能夠擁有一個JobDataMap,其關聯的Job能夠經過JobExecutionContext#getTrigger().getJobDataMap()獲取Trigger中的JobDataMap(該JobDataMap和JobDetail所擁有的JobDataMap不是同一個哦)。無論是有狀態仍是無狀態的任務,在任務執行期間對Trigger的JobDataMap所作的更改都不會進行持久,也即不會對下次的執行產生影響。
Quartz擁有完善的事件和監聽體系,大部分組件都擁有事件,如任務執行前事件、任務執行後事件、觸發器觸發前事件、觸發後事件、調度器開始事件、關閉事件等等,能夠註冊相應的監聽器處理感興趣的事件。
圖1描述了Scheduler的內部組件結構,SchedulerContext提供Scheduler全局可見的上下文信息,每個任務都對應一個JobDataMap,虛線表達的JobDataMap表示對應有狀態的任務:
Scheduler結構圖
一個Scheduler能夠擁有多個Triger組和多個JobDetail組,註冊Trigger和JobDetail時,若是不顯式指定所屬的組,Scheduler將放入到默認組中,默認組的組名爲Scheduler.DEFAULT_GROUP。組名和名稱組成了對象的全名,同一類型對象的全名不能相同。
Scheduler自己就是一個容器,它維護着Quartz的各類組件並實施調度的規則。Scheduler還擁有一個線程池,線程池爲任務提供執行線程——這比執行任務時簡單地建立一個新線程要擁有更高的效率,同時經過共享節約資源的佔用。經過線程池組件的支持,對於繁忙度高、壓力大的任務調度,Quartz將能夠提供良好的伸縮性。
提示: Quartz完整下載包examples目錄下擁有10多個實例,它們是快速掌握Quartz應用很好的實例。
使用SimpleTrigger
SimpleTrigger擁有多個重載的構造函數,用以在不一樣場合下構造出對應的實例:
●SimpleTrigger(String name, String group):經過該構造函數指定Trigger所屬組和名稱;
●SimpleTrigger(String name, String group, Date startTime):除指定Trigger所屬組和名稱外,還能夠指定觸發的開發時間;
●SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval):除指定以上信息外,還能夠指定結束時間、重複執行次數、時間間隔等參數;
●SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval):這是最複雜的一個構造函數,在指定觸發參數的同時,還經過jobGroup和jobName,讓該Trigger和Scheduler中的某個任務關聯起來。
Cron表達式
Quartz使用相似於Linux下的Cron表達式定義時間規則,Cron表達式由6或7個由空格分隔的時間字段組成,如表1所示:
Cron表達式時間字段
位置 |
時間域名 |
容許值 |
容許的特殊字符 |
1 |
秒 |
0-59 |
, - * / |
2 |
分鐘 |
0-59 |
, - * / |
3 |
小時 |
0-23 |
, - * / |
4 |
日期 |
1-31 |
, - * ? / L W C |
5 |
月份 |
1-12 |
, - * / |
6 |
星期 |
1-7 |
, - * ? / L C # |
7 |
年(可選) |
空值1970-2099 |
, - * / |
Cron表達式的時間字段除容許設置數值外,還可以使用一些特殊的字符,提供列表、範圍、通配符等功能,細說以下:
●星號(*):可用在全部字段中,表示對應時間域的每個時刻,例如,*在分鐘字段時,表示「每分鐘」;
●問號(?):該字符只在日期和星期字段中使用,它一般指定爲「無心義的值」,至關於點位符;
●減號(-):表達一個範圍,如在小時字段中使用「10-12」,則表示從10到12點,即10,11,12;
●逗號(,):表達一個列表值,如在星期字段中使用「MON,WED,FRI」,則表示星期一,星期三和星期五;
●斜槓(/):x/y表達一個等步長序列,x爲起始值,y爲增量步長值。如在分鐘字段中使用0/15,則表示爲0,15,30和45秒,而5/15在分鐘字段中表示5,20,35,50,你也可使用*/y,它等同於0/y;
●L:該字符只在日期和星期字段中使用,表明「Last」的意思,但它在兩個字段中意思不一樣。L在日期字段中,表示這個月份的最後一天,如一月的31號,非閏年二月的28號;若是L用在星期中,則表示星期六,等同於7。可是,若是L出如今星期字段裏,並且在前面有一個數值X,則表示「這個月的最後X天」,例如,6L表示該月的最後星期五;
●W:該字符只能出如今日期字段裏,是對前導日期的修飾,表示離該日期最近的工做日。例如15W表示離該月15號最近的工做日,若是該月15號是星期六,則匹配14號星期五;若是15日是星期日,則匹配16號星期一;若是15號是星期二,那結果就是15號星期二。但必須注意關聯的匹配日期不可以跨月,如你指定1W,若是1號是星期六,結果匹配的是3號星期一,而非上個月最後的那天。W字符串只能指定單一日期,而不能指定日期範圍;
●LW組合:在日期字段能夠組合使用LW,它的意思是當月的最後一個工做日;
●井號(#):該字符只能在星期字段中使用,表示當月某個工做日。如6#3表示當月的第三個星期五(6表示星期五,#3表示當前的第三個),而4#5表示當月的第五個星期三,假設當月沒有第五個星期三,忽略不觸發;
● C:該字符只在日期和星期字段中使用,表明「Calendar」的意思。它的意思是計劃所關聯的日期,若是日期沒有被關聯,則至關於日曆中全部日期。例如5C在日期字段中就至關於日曆5日之後的第一天。1C在星期字段中至關於星期往後的第一天。
Cron表達式對特殊字符的大小寫不敏感,對錶明星期的縮寫英文大小寫也不敏感。
Cron表示式示例
表示式 |
說明 |
"0 0 12 * * ? " |
天天12點運行 |
"0 15 10 ? * *" |
天天10:15運行 |
"0 15 10 * * ?" |
天天10:15運行 |
"0 15 10 * * ? *" |
天天10:15運行 |
"0 15 10 * * ? 2008" |
在2008年的天天10:15運行 |
"0 * 14 * * ?" |
天天14點到15點之間每分鐘運行一次,開始於14:00,結束於14:59。 |
"0 0/5 14 * * ?" |
天天14點到15點每5分鐘運行一次,開始於14:00,結束於14:55。 |
"0 0/5 14,18 * * ?" |
天天14點到15點每5分鐘運行一次,此外天天18點到19點每5鍾也運行一次。 |
"0 0-5 14 * * ?" |
天天14:00點到14:05,每分鐘運行一次。 |
"0 10,44 14 ? 3 WED" |
3月每週三的14:10分到14:44,每分鐘運行一次。 |
"0 15 10 ? * MON-FRI" |
每週一,二,三,四,五的10:15分運行。 |
"0 15 10 15 * ?" |
每個月15日10:15分運行。 |
"0 15 10 L * ?" |
每個月最後一天10:15分運行。 |
"0 15 10 ? * 6L" |
每個月最後一個星期五10:15分運行。 |
"0 15 10 ? * 6L 2007-2009" |
在2007,2008,2009年每月的最後一個星期五的10:15分運行。 |
"0 15 10 ? * 6#3" |
每個月第三個星期五的10:15分運行。 |
代碼示例
任務調度信息存儲
在默認狀況下Quartz將任務調度的運行信息保存在內存中,這種方法提供了最佳的性能,由於內存中數據訪問最快。不足之處是缺少數據的持久性,當程序路途中止或系統崩潰時,全部運行的信息都會丟失。好比咱們但願安排一個執行100次的任務,若是執行到50次時系統崩潰了,系統重啓時任務的執行計數器將從0開始。在大多數實際的應用中,咱們每每並不須要保存任務調度的現場數據,由於不多須要規劃一個指定執行次數的任務。
對於僅執行一次的任務來講,其執行條件信息自己應該是已經持久化的業務數據(如鎖定到期解鎖任務,解鎖的時間應該是業務數據),當執行完成後,條件信息也會相應改變。固然調度現場信息不只僅是記錄運行次數,還包括調度規則、JobDataMap中的數據等等。
若是確實須要持久化任務調度信息,Quartz容許你經過調整其屬性文件,將這些信息保存到數據庫中。使用數據庫保存任務調度信息後,即便系統崩潰後從新啓動,任務的調度信息將獲得恢復。如前面所說的例子,執行50次崩潰後從新運行,計數器將從51開始計數。使用了數據庫保存信息的任務稱爲持久化任務。
經過配置文件調整任務調度信息的保存策略
其實Quartz JAR文件的org.quartz包下就包含了一個quartz.properties屬性配置文件並提供了默認設置。若是須要調整默認配置,能夠在類路徑下創建一個新的quartz.properties,它將自動被Quartz加載並覆蓋默認的設置。
先來了解一下Quartz的默認屬性配置文件:
quartz.properties:默認配置
Quartz的屬性配置文件主要包括三方面的信息:
1)集羣信息;
2)調度器線程池;
3)任務調度現場數據的保存。
若是任務數目很大時,能夠經過增大線程池的大小獲得更好的性能。默認狀況下,Quartz採用org.quartz.simpl.RAMJobStore保存任務的現場數據,顧名思義,信息保存在RAM內存中,咱們能夠經過如下設置將任務調度現場數據保存到數據庫中:
quartz.properties:使用數據庫保存任務調度現場數據
要將任務調度數據保存到數據庫中,就必須使用org.quartz.impl.jdbcjobstore.JobStoreTX代替原來的org.quartz.simpl.RAMJobStore並提供相應的數據庫配置信息。首先①處指定了Quartz數據庫表的前綴,在②處定義了一個數據源,在③處具體定義這個數據源的鏈接信息。
你必須事先在相應的數據庫中建立Quartz的數據表(共8張),在Quartz的完整發布包的docs/dbTables目錄下擁有對應不一樣數據庫的SQL腳本。
查詢數據庫中的運行信息
任務的現場保存對於上層的Quartz程序來講是徹底透明的,咱們在src目錄下編寫一個如上代碼所示的quartz.properties文件後,從新運行以下代碼示例的程序,在數據庫表中將能夠看到對應的持久化信息。當調度程序運行過程當中途中止後,任務調度的現場數據將記錄在數據表中,在系統重啓時就能夠在此基礎上繼續進行任務的調度。
當代碼SimpleTriggerRunner執行到一段時間後非正常退出,咱們就能夠經過這個JDBCJobStoreRunner根據記錄在數據庫中的現場數據恢復任務的調度。Scheduler中的全部Trigger以及JobDetail的運行信息都會保存在數據庫中,這裏咱們僅恢復tgroup1組中名稱爲trigger1_1的觸發器,這能夠經過所示的代碼進行過濾,觸發器的採用GROUP.TRIGGER_NAME的全名格式。經過Scheduler#rescheduleJob(String triggerName,String groupName,Trigger newTrigger)便可從新調度關聯某個Trigger的任務。
下面咱們來觀察一下不一樣時期qrtz_simple_triggers表的數據:
1.運行代碼SimpleTriggerRunner一小段時間後退出:
REPEAT_COUNT表示須要運行的總次數,而TIMES_TRIGGER表示已經運行的次數。
2.運行代碼JDBCJobStoreRunner恢復trigger1_1的觸發器,運行一段時間後退出,這時qrtz_simple_triggers中的數據以下:
首先Quartz會將原REPEAT_COUNT-TIMES_TRIGGER獲得新的REPEAT_COUNT值,並記錄已經運行的次數(從新從0開始計算)。
3.從新啓動JDBCJobStoreRunner運行後,數據又將發生相應的變化:
4.繼續運行直至完成全部剩餘的次數,再次查詢qrtz_simple_triggers表:
這時,該表中的記錄已經變空。
值得注意的是,若是你使用JDBC保存任務調度數據時,當你運行代碼SimpleTriggerRunner而後退出,當再次但願運行SimpleTriggerRunner時,系統將拋出JobDetail重名的異常:Unable to store Job with name: 'job1_1' and group: 'jGroup1', because one already exists with this identification.
由於每次調用Scheduler#scheduleJob()時,Quartz都會將JobDetail和Trigger的信息保存到數據庫中,若是數據表中已經同名的JobDetail或Trigger,異常就產生了。
本文使用quartz 1.6版本,咱們發現當後臺數據庫使用MySQL時,數據保存不成功,該錯誤是Quartz的一個Bug,相信會在高版本中獲得修復。由於HSQLDB不支持SELECT * FROM TABLE_NAME FOR UPDATE的語法,因此不能使用HSQLDB數據庫。
附錄
·org.quartz.scheduler.instanceName
每一個 Scheduler 必須給定一個名稱來標識。當在同一個程序中有多個實例時,這個名稱做爲客戶代碼識別是哪一個 Scheduler 而用。假如你用到了集羣特性,你就必須爲集羣中的每個實例使用相同的名稱,以使它們成爲「邏輯上」 是同一個 Scheduler 。
·org.quartz.scheduler.instanceId
每一個 Quartz Scheduler 必須指定一個惟一的 ID。這個值能夠是任何字符串值,只要對於全部的 Scheduler 是惟一的。若是你想要自動生成的 ID,那你可使用AUTO 做爲 instanceId 。從版本 1.5.1 開始,你可以定製如何自動生成實例 ID。見instanceIDGenerator.class 屬性,會在接下來說到。
·org.quartz.scheduler.instanceIdGenerator.class
從版本 1.5.1 開始,這個屬性容許你定製instanceId 的生成,這個屬性僅被用於屬性org.quartz.scheduler.instanceId 設置爲AUTO 的狀況下。默認是 org.quartz.simpl.SimpleInstanceIdGenerator ,它會基於主機名和時間戳來產生實例 ID 的。
·org.quartz.scheduler.threadName
能夠是對於 Java 線程來講有效名稱的任何字符串。假如這個屬性未予指定,線程將會接受 Scheduler 名稱 (org.quartz.scheduler.instanceName ) 前附加上字符串 '_QuartzSchedulerThread' 做爲名稱。
·org.quartz.scheduler.idelWaitTime
這個屬性設置了當 Scheduler 處於空閒時轉而再次查詢可用 Trigger 時所等待的毫秒數。一般,你無需調整這個參數,除非你正使用 XA 事物,遇到了 Trigger 本該當即觸發而發生延遲的問題。
·org.quartz.scheduler.dbFailureRetryInterval
這個屬性設置 Scheduler 在檢測到 JobStore 到某處的鏈接(好比到數據庫的鏈接) 斷開後,再次嘗試鏈接所等待的毫秒數。這個參數在使用 RamJobStore 無效。
·org.quartz.scheduler.classLoadHelper.class
對於多數健狀的應用,所使用的默認值爲 org.quartz.simpl.CascadingClassLoadHelper 類,它會依序使用其餘的ClassLoadHelper 類,直到有一個能正常工做爲止。你大概沒必須爲這個屬性指定任何其餘的類,除非有可能在應用服務器中時。當前全部可能的ClassLoadHelper 實現可在 org.quartz.simpl 包中找到。
·org.quartz.context.key.SOME_KEY
這個屬性用於向 "Scheduler 上下文" 中置入一個 名-值 對錶示的字符串值。(見 Scheduler.getContext() )。所以,好比設置了org.quartz.context.key.MyEmail = myemail@somehost.com就至關於執行了scheduler.getContext().put("MyEmail", myemail@somehost.com)
·org.quartz.scheduler.userTransactionURL
它設置了 Quartz 能在哪裏定位到應用服務器的 UserTransaction 管理器的 JNDI URL。默認值(未設定的話) 是java:comp/UserTransaction ,這幾乎能工做於全部的應用服務器中。Websphere 用戶也許須要設置這個屬性爲jta/usertransaction 。這個屬性僅用於 Quartz 配置使用 JobStoreCMT 的狀況,而且org.quartz.scheduler.wrapJobExecutionInUserTransaction 被設定成了true 。
·org.quartz.scheduler.wrapJobExecutionInUserTransaction
若是你要 Quartz 在調用你的 Job 的 execute 以前啓動一個 UserTransaction 的話,設置這個屬性爲 true 。這個事物將在 Job 的execute 方法完成和 JobDataMap (假如是一個StatefulJob ) 更新後提交。默認值爲 false 。
·org.quartz.scheduler.jobFactory.class
這是所用的 JobFactory 的類名稱。默認爲 org.quartz.simpl.SimpleJobFactory 。你也能夠試試org.quartz.simpl.PropertySettingJobFactory 。一個 Job 工廠負責產生 Job 類的實例。SimpleFactory 類是調用 Job 類的newInstance() 方法。PropertySettingJobFactory 也會調用newInstance() ,但還會使用 JobDataMap 中的內容以反射方式設置 Job Bean 的屬性。
小結
Quartz提供了最爲豐富的任務調度功能,不但能夠制定週期性運行的任務調度方案,還可讓你按照日曆相關的方式進行任務調度。Quartz框架的重要組件包括Job、JobDetail、Trigger、Scheduler以及輔助性的JobDataMap和SchedulerContext。Quartz擁有一個線程池,經過線程池爲任務提供執行線程,你能夠經過配置文件對線程池進行參數定製。Quartz的另外一個重要功能是可將任務調度信息持久化到數據庫中,以便系統重啓時可以恢復已經安排的任務。此外,Quartz還擁有完善的事件體系,容許你註冊各類事件的監聽器。