1. 程式人生 > >任務排程框架Quartz(一) Quartz——一個強大的定時任務排程框架

任務排程框架Quartz(一) Quartz——一個強大的定時任務排程框架

Quartz,水晶、石英,一個簡單樸素有美麗的名字,在Java程式界,Quartz大名鼎鼎,很多Java應用幾乎都整合或構建了一個定時任務排程系統,Quartz是一個定時任務排程框架。

何為定時任務排程框架?簡而言之,它可以領會我們的意圖在未來某個時刻做我們想要做的事情,比如,女友生日那天定時傳送簡訊討好下(當然,除此之外,你還要買買買…)。

我們的應用程式有些定時任務(例如想在凌晨十二點半統計某個網際網路金融公司一款借款APP前一天的借款、還款以及逾期情況)需要在指定時間內執行或者週期性執行某個任務(比如每月最後一天統計這個月的財務報表給財務部門等),這時候我們就需要用到任務排程框架了。Quartz正是一個炙手可熱的任務排程框架,它簡單易上手,並且可以與Spring整合(這才是重點)。

現在,我們帶著疑問開始認識Quartz…

基本問題

Quartz是什麼?

Quartz是一個任務排程框架(庫),它幾乎可以整合到任何應用系統中。術語”job schedule”似乎為不同的人提供了不同的想法。當你閱讀該教程時,你應該能夠得到一個堅定的想法關於我們使用這個術語時表達含義,但總之,作業排程是負責執行(或通知)其他軟體元件在預定時間執行的服務元件。

Quartz是非常靈活的,幷包含多個使用範例,它們可以單獨或一起使用,以實現您所期望的行為,並使您能夠以最“自然”的方式來編寫您的專案的程式碼。

Quartz是非常輕量級的,只需要非常少的配置 —— 它實際上可以被跳出框架來使用,如果你的需求是一些相對基本的簡單的需求的話。

Quartz具有容錯機制,並且可以在重啟服務的時候持久化(”記憶”)你的定時任務,你的任務也不會丟失。

雖然通過schedule可以簡單實現一些系統任務定時執行,當您學習如何使用它來驅動應用程式的業務流程的流程時,Quartz的全部潛力是可以實現的。

Quartz又不是什麼?

Quartz不是一個任務佇列——雖然它確實可以在一些小規模應用中合理的作為一個任務佇列來使用。

Quartz不是一個網格計算引擎——雖然在某些小規模應用中,這樣做確實可以達到應用的要求(定時計算、統計一些資料)。

Quartz不是一個提供給業務人員的執行服務——它是一個庫,很容易整合到應用程式中去做一些未來某時刻可能會一直迴圈執行的相關的任務。

從一個軟體元件角度來看Quartz是什麼?

Quartz是一個很輕量級的java庫,幾乎包含了所有定時的功能。主要介面是Schedule,提供了一些簡單的操作:安排任務或取消任務,啟動或者停止任務。

如果你想在應用中使用Quartz,應該實現Job介面,包含了一個execute()方法。如果你想在一個任務執行時間到了的時候通知你,元件應該實現TriggerListener 或者JobListener 介面。

Quartz任務可以在你的應用中啟動和執行,可以作為一個獨立的應用程式(通過RMI介面),也可是在一個J2EE應用中執行。

為什麼不簡單的使用just use java.util.Timer就行了呢?

從JDK1.3開始,Java通過java.util.Timerjava.util.TimerTask可以實現定時器。為什麼要使用Quartz而不是使用Java中的這些標準功能呢?

原因太多了,這裡列舉幾個:

  • Timers沒有持久化機制.
  • Timers不靈活 (只可以設定開始時間和重複間隔,不是基於時間、日期、天等(秒、分、時)的)
  • Timers 不能利用執行緒池,一個timer一個執行緒
  • Timers沒有真正的管理計劃

還有什麼是可以替代Quartz的?

其他問題

Quartz可以執行多少任務?

這是一個很難回答的問題…答案基本上是“它取決於使用情況”
我知道你討厭這樣的答案,所以這裡的一些資訊:

首先,你使用的JobStore扮演著一個重要的因素。基於RAM的JobStore(100x)比基於JDBC的JobStore更快近乎100倍。JDBC JobStore速度幾乎完全取決於您的資料庫的連線速度,使用的資料庫系統,以及資料庫執行的硬體裝置,Quartz幾乎全部時間花在資料庫上了。當然RAMJobStore儲存多少JobTriggers也是有限制的,你使用的空間肯定比資料庫的硬碟空間要小了。你可以檢視“如何提升JDBC-JobStore的效能”的問題。

因此,限制JobTriggers可以儲存或監聽的數量的因素是儲存空間的大小(RAM的數量和磁碟空間大小)。

關於通過RMI使用Quartz的問題

RMI是有問題的,特別是你如果不清楚通過RMI機制時類是如何載入的話。強烈建議讀讀所有關於RMI的java API。強烈建議您閱讀以下的參考資料:
一個關於RMI和程式碼庫的極好描述: http://www.kedwards.com/jini/codebase.html。很重要的一點是要意識到“程式碼”是由客戶端使用!

關於Job的一些問題

如何控制Job的例項?

可以檢視org.quartz.spi.JobFactoryorg.quartz.Scheduler.setJobFactory(..) 的方法。

當一個Job完成並移除之後,還能儲存嗎?

設定屬性:JobDetail.setDurability(true)——當job不再有trigger引用它的時候,Quartz也不要刪除job。

如何保證一個job併發執行?

實現StatefulJob 而不是Job,檢視更多關於StatefulJob 的資訊。

如何停止一個正在執行的Job?

檢視org.quartz.InterruptableJob介面和Scheduler.interrupt(String, String)方法。

關於Trigger的一些問題

怎麼執行任務鏈?或者說怎麼建立工作流?

目前還沒有直接的或自由的方式通過Quartz鏈式觸發任務。然而,有幾種方法,你可以輕易的達到目標。

方法一:
使用監聽器(TriggerListener,JobListener 或者SchedulerListener),可以通知到某個工作完成,然後可以開始另一個任務。這種方式有一點複雜,你必須告知監聽器哪個任務是接著哪個任務的,你可能還會擔心這些資訊的永續性。可以檢視org.quartz.listeners.JobChainingJobListener,已經具有一些這樣的功能了。
方法二:
建立一個Job,它的JobDataMap 包含下一個Job的名字,當這一個job執行完畢再執行下一個任務。大多數的做法是建立一個基類(通常是抽象類或介面)作為Job,並擁有獲得job名稱的方法,使用預定義的key從JobDataMap 進行分組。抽象類實現execute()方法委託模板方法例如”doWork()”去執行,它包含了排程後續作業的程式碼。之後子類要做的只是簡單的擴充套件這個類,包括做自己應該做的工作。持久化job的使用,或者過載addJob(JobDetail, boolean, boolean) 方法(Qartz2.2新增的)幫助應用程式使用適當的資料來定義所有的工作,並沒有建立觸發器來激發他們(除了一個觸發器觸發外鏈中的第一個job)。
以後,Quartz 將會提供一個更簡潔的方式處理這個流程,但是現在你可以考慮前面兩種處理方式或其他更好的方式處理工作流。

為什麼我的觸發器trigger沒有執行?

常見的原因可能是沒有呼叫Scheduler.start()方法,這個方法它告訴排程程式啟動觸發器。還有一種可能是trigger或者trigger group被暫停了。

夏令時和觸發器

CronTrigger SimpleTrigger以自己的方式處理夏令時——每一個方式,都是直觀的觸發型別。

首先,瞭解下什麼是夏令時,可以檢視:https://secure.wikimedia.org/wikipedia/en/wiki/Daylight_saving_time_around_the_world。一些讀者可能沒意識到,不同的國家/內容的規則是不同的。舉個例子,2005年的夏令時起始於4月3日(美國)而埃及是4月29。同樣重要的是要知道,不同地區不僅僅是日期不同,日期轉換(前一天和後一天的中國是零點)也是不同的。許多地方移在凌晨兩點,但其他地方可能是凌晨1:00,凌晨3點等。

SimpleTrigger可以讓你安排一個任務在任何毫秒級執行。可以每N毫秒執行一次任務。總是每N秒就發生一次,與一天中的時間沒有關係。
CronTrigger可以讓你在某些時刻執行任務,是按”公曆”時間計算的。在指定的一天中的時間觸發,然後計算下一次觸發的時間。

關於JDBCJobStore的一些問題

怎麼提升JDBC-JobStore的效能?

下面有一些提升JDBC-JobStore效能的方法,其中只有一種是有效的:

  • 使用更快更好的網路
  • 買一個更好的機器
  • 買一個更好的RDBMS

現在,提供一種簡單的但有效的方式:在Quartz表建立索引。

很多資料庫系統都會自動把索引放在主鍵欄位上,許多資料庫系統也會對外來鍵也建立索引。確保你的資料庫是這樣做的,或者手動為表的關鍵字建立索引。
最重要的索引的TRIGGER 表的next_fire_timestate欄位。最後但不是重要的,為FIRED_TRIGGERS 表的每一個欄位設定索引。

create index idx_qrtz_t_next_fire_time on qrtz_triggers(NEXT_FIRE_TIME);
create index idx_qrtz_t_state on qrtz_triggers(TRIGGER_STATE);
create index idx_qrtz_t_nf_st on qrtz_triggers(TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_ft_trig_name on qrtz_fired_triggers(TRIGGER_NAME);
create index idx_qrtz_ft_trig_group on qrtz_fired_triggers(TRIGGER_GROUP);
create index idx_qrtz_ft_trig_name on qrtz_fired_triggers(TRIGGER_NAME);
create index idx_qrtz_ft_trig_n_g on \
    qrtz_fired_triggers(TRIGGER_NAME,TRIGGER_GROUP);
create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(INSTANCE_NAME);
create index idx_qrtz_ft_job_name on qrtz_fired_triggers(JOB_NAME);
create index idx_qrtz_ft_job_group on qrtz_fired_triggers(JOB_GROUP);
create index idx_qrtz_t_next_fire_time_misfire on \
    qrtz_triggers(MISFIRE_INSTR,NEXT_FIRE_TIME);
create index idx_qrtz_t_nf_st_misfire on \
    qrtz_triggers(MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
create index idx_qrtz_t_nf_st_misfire_grp on \
    qrtz_triggers(MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);

叢集功能最適合於長時間執行和/或密集的工作(在多個節點上分配工作負載),如果你需要擴充套件到支援成千上萬的短執行(例如1秒)的工作,考慮工作集分割使用多個不同的排程器(因此多套表(有不同的字首))。當你新增多個客戶端的時候,使用一個排程程式將會強制使用一個叢集鎖,一個模式,降低效能。

如果資料庫伺服器重新啟動,我的資料庫連線不會恢復正常

如果您正在建立連線資料來源(通過指定在Quartz屬性檔案中的連線引數),請確保您有一個指定的連線驗證查詢:

org.quartz.dataSource.myDS.validationQuery=select 0 from dual

這個專門的查詢語句對Orable資料庫是非常有效的。其他的資料庫,可以使用合適的sql。

如果你的資料來源是由您的應用程式伺服器管理,確保資料來源配置在這樣一種方式,它可以檢測連線失敗。

關於Transactions事務的一些問題

使用JobStoreCMT,並且遇到死鎖,該怎麼辦?

JobStoreCMT 在大量使用,濫用可以導致死鎖。不管怎樣,我們總是抱怨死鎖。到目前為止,該問題已被證明是“使用者錯誤”。如果遇到死鎖,下面的列表可能是你需要檢查的事情了:

  • 當一個事務執行很長時間時,有些資料庫會把它當成死鎖。 確保你已經設定了索引 。
  • 確保在你的執行緒池中至少有兩個以上資料庫連線。
  • 確保你有一個託管的和非託管的資料來源供Quartz使用。
  • 確保你在一個任務中處理的業務是在一個事務中。 處理完記得提交事務。
  • 如果你的 Jobs的 execute()方法 使用schedule, 確保使用UserTransaction或者設定Quartz屬性:"org.quartz.scheduler.wrapJobExecutionInUserTransaction=true".

叢集、規模化和高可用特性

Quartz有什麼叢集能力?

其他的叢集配置,不依賴後臺資料庫,Terracotta