quartz集羣

其實主要仍是借鑑別人的東東,寫的還挺好的。我本身只在最後說說本身遇到的坑吧。java

一、Quartz任務調度的基本實現原理

  Quartz是OpenSymphony開源組織在任務調度領域的一個開源項目,徹底基於Java實現。做爲一個優秀的開源調度框架,Quartz具備如下特色:mysql

    (1)強大的調度功能,例如支持豐富多樣的調度方法,能夠知足各類常規及特殊需求;算法

    (2)靈活的應用方式,例如支持任務和調度的多種組合方式,支持調度數據的多種存儲方式;spring

    (3)分佈式和集羣能力,Terracotta收購後在原來功能基礎上做了進一步提高。本文將對該部分相加闡述。sql

1.1 Quartz 核心元素

  Quartz任務調度的核心元素爲:Scheduler——任務調度器、Trigger——觸發器、Job——任務。其中trigger和job是任務調度的元數據,scheduler是實際執行調度的控制器。數據庫

  Trigger是用於定義調度時間的元素,即按照什麼時間規則去執行任務。Quartz中主要提供了四種類型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。這四種trigger能夠知足企業應用中的絕大部分需求。apache

  Job用於表示被調度的任務。主要有兩種類型的job:無狀態的(stateless)和有狀態的(stateful)。對於同一個trigger來講,有狀態的job不能被並行執行,只有上一次觸發的任務被執行完以後,才能觸發下一次執行。Job主要有兩種屬性:volatility和durability,其中volatility表示任務是否被持久化到數據庫存儲,而durability表示在沒有trigger關聯的時候任務是否被保留。二者都是在值爲true的時候任務被持久化或保留。一個job能夠被多個trigger關聯,可是一個trigger只能關聯一個job。服務器

  Scheduler由scheduler工廠建立:DirectSchedulerFactory或者StdSchedulerFactory。第二種工廠StdSchedulerFactory使用較多,由於DirectSchedulerFactory使用起來不夠方便,須要做許多詳細的手工編碼設置。Scheduler主要有三種:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。架構

  Quartz核心元素之間的關係如圖1.1所示:mvc

圖1.1 核心元素關係圖

1.2 Quartz 線程視圖

  在Quartz中,有兩類線程,Scheduler調度線程和任務執行線程,其中任務執行線程一般使用一個線程池維護一組線程。

圖1.2 Quartz線程視圖

  Scheduler調度線程主要有兩個:執行常規調度的線程,和執行misfiredtrigger的線程。常規調度線程輪詢存儲的全部trigger,若是有須要觸發的trigger,即到達了下一次觸發的時間,則從任務執行線程池獲取一個空閒線程,執行與該trigger關聯的任務。Misfire線程是掃描全部的trigger,查看是否有misfiredtrigger,若是有的話根據misfire的策略分別處理(fire now OR wait for the next fire)。

1.3 Quartz Job數據存儲

  Quartz中的trigger和job須要存儲下來才能被使用。Quartz中有兩種存儲方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是將trigger和job存儲在內存中,而JobStoreSupport是基於jdbc將trigger和job存儲到數據庫中。RAMJobStore的存取速度很是快,可是因爲其在系統被中止後全部的數據都會丟失,因此在集羣應用中,必須使用JobStoreSupport。

二、Quartz集羣原理

2.1 Quartz 集羣架構

  一個Quartz集羣中的每一個節點是一個獨立的Quartz應用,它又管理着其餘的節點。這就意味着你必須對每一個節點分別啓動或中止。Quartz集羣中,獨立的Quartz節點並不與另外一其的節點或是管理節點通訊,而是經過相同的數據庫表來感知到另外一Quartz應用的,如圖2.1所示。

圖2.1 Quartz集羣架構

2.2 Quartz集羣相關數據庫表

  由於Quartz集羣依賴於數據庫,因此必須首先建立Quartz數據庫表,Quartz發佈包中包括了全部被支持的數據庫平臺的SQL腳本。這些SQL腳本存放於<quartz_home>/docs/dbTables 目錄下。這裏採用的Quartz 1.8.4版本,總共12張表,不一樣版本,表個數可能不一樣。數據庫爲mysql,用tables_mysql.sql建立數據庫表。所有表如圖2.2所示,對這些表的簡要介紹如圖2.3所示。

 

圖2.2 Quartz 1.8.4在mysql數據庫中生成的表

圖2.3 Quartz數據表簡介

2.2.1 調度器狀態表(QRTZ_SCHEDULER_STATE)

  說明:集羣中節點實例信息,Quartz定時讀取該表的信息判斷集羣中每一個實例的當前狀態。

  instance_name配置文件中org.quartz.scheduler.instanceId配置的名字,若是設置爲AUTO,quartz會根據物理機名和當前時間產生一個名字。

  last_checkin_time上次檢入時間

  checkin_interval檢入間隔時間

2.2.2 觸發器與任務關聯表(qrtz_fired_triggers)

  存儲與已觸發的Trigger相關的狀態信息,以及相聯Job的執行信息。

2.2.3 觸發器信息表(qrtz_triggers)

  trigger_nametrigger的名字,該名字用戶本身能夠隨意定製,無強行要求

  trigger_grouptrigger所屬組的名字,該名字用戶本身隨意定製,無強行要求

  job_nameqrtz_job_details表job_name的外鍵

  job_groupqrtz_job_details表job_group的外鍵

  trigger_state當前trigger狀態設置爲ACQUIRED,若是設爲WAITING,則job不會觸發

  trigger_cron觸發器類型,使用cron表達式

2.2.4 任務詳細信息表(qrtz_job_details)

  說明:保存job詳細信息,該表須要用戶根據實際狀況初始化

  job_name集羣中job的名字,該名字用戶本身能夠隨意定製,無強行要求。

  job_group集羣中job的所屬組的名字,該名字用戶本身隨意定製,無強行要求。

  job_class_name集羣中job實現類的徹底包名,quartz就是根據這個路徑到classpath找到該job類的。

  is_durable是否持久化,把該屬性設置爲1,quartz會把job持久化到數據庫中

  job_data一個blob字段,存放持久化job對象。

2.2.5權限信息表(qrtz_locks)

  說明:tables_oracle.sql裏有相應的dml初始化,如圖2.4所示。

圖2.4 Quartz權限信息表中的初始化信息

2.3 Quartz Scheduler在集羣中的啓動流程

  Quartz Scheduler自身是察覺不到被集羣的,只有配置給Scheduler的JDBC JobStore才知道。當Quartz Scheduler啓動時,它調用JobStore的schedulerStarted()方法,它告訴JobStore Scheduler已經啓動了。schedulerStarted() 方法是在JobStoreSupport類中實現的。JobStoreSupport類會根據quartz.properties文件中的設置來肯定Scheduler實例是否參與到集羣中。假如配置了集羣,一個新的ClusterManager類的實例就被建立、初始化並啓動。ClusterManager是在JobStoreSupport類中的一個內嵌類,繼承了java.lang.Thread,它會按期運行,並對Scheduler實例執行檢入的功能。Scheduler也要查看是否有任何一個別的集羣節點失敗了。檢入操做執行週期在quartz.properties中配置。

2.4 偵測失敗的Scheduler節點

  當一個Scheduler實例執行檢入時,它會查看是否有其餘的Scheduler實例在到達他們所預期的時間還未檢入。這是經過檢查SCHEDULER_STATE表中Scheduler記錄在LAST_CHEDK_TIME列的值是否早於org.quartz.jobStore.clusterCheckinInterval來肯定的。若是一個或多個節點到了預約時間尚未檢入,那麼運行中的Scheduler就假定它(們) 失敗了。

2.5 從故障實例中恢復Job

  當一個Sheduler實例在執行某個Job時失敗了,有可能由另外一正常工做的Scheduler實例接過這個Job從新運行。要實現這種行爲,配置給JobDetail對象的Job可恢復屬性必須設置爲true(job.setRequestsRecovery(true))。若是可恢復屬性被設置爲false(默認爲false),當某個Scheduler在運行該job失敗時,它將不會從新運行;而是由另外一個Scheduler實例在下一次觸發時間觸發。Scheduler實例出現故障後多快能被偵測到取決於每一個Scheduler的檢入間隔(即2.3中提到的org.quartz.jobStore.clusterCheckinInterval)。

三、Quartz集羣實例(Quartz+Spring)

3.1 Spring不兼容Quartz問題

  Spring從2.0.2開始便再也不支持Quartz。具體表如今 Quartz+Spring 把 Quartz 的 Task 實例化進入數據庫時,會產生: Serializable 的錯誤:

<bean id="jobtask" class="org.springframework.scheduling.quartz. MethodInvokingJobDetailFactoryBean ">
  <property name="targetObject">
    <ref bean="quartzJob"/>
  </property>
  <property name="targetMethod">
    <value>execute</value>
  </property>
</bean>

  這個 MethodInvokingJobDetailFactoryBean 類中的 methodInvoking 方法,是不支持序列化的,所以在把 QUARTZ 的 TASK 序列化進入數據庫時就會拋錯。

  首先解決MethodInvokingJobDetailFactoryBean的問題,在不修改Spring源碼的狀況下,能夠避免使用這個類,直接調用JobDetail。可是使用JobDetail實現,須要本身實現MothodInvoking的邏輯,可使用JobDetail的jobClass和JobDataAsMap屬性來自定義一個Factory(Manager)來實現一樣的目的。例如,本示例中新建了一個MyDetailQuartzJobBean來實現這個功能。

3.2 AutohomeDetailQuartzJobBean.java文件

package org.lxh.mvc.jobbean;

import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class AutohomeDetailQuartzJobBean extends QuartzJobBean {
    protected final Log logger = LogFactory.getLog(getClass());
    private String targetObject;
    private String targetMethod;
    private ApplicationContext ctx;
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        try {
            logger.info("execute [" + targetObject + "] at once>>>>>>");
            Object otargetObject = ctx.getBean(targetObject);
            Method m = null;
            try {
                m = otargetObject.getClass().getMethod(targetMethod, new Class[] {});
                m.invoke(otargetObject, new Object[] {});
            } catch (SecurityException e) {
                logger.error(e);
            } catch (NoSuchMethodException e) {
                logger.error(e);
            }
        } catch (Exception e) {
            throw new JobExecutionException(e);
        }
    }

    public void setApplicationContext(ApplicationContext applicationContext){
        this.ctx=applicationContext;
    }

    public void setTargetObject(String targetObject) {
        this.targetObject = targetObject;
    }

    public void setTargetMethod(String targetMethod) {
        this.targetMethod = targetMethod;
    }

}
package org.lxh.mvc.job;
import java.io.Serializable;
import java.util.Date;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Test implements Serializable{
    private Log logger = LogFactory.getLog(Test.class);
    private static final long serialVersionUID = -2073310586499744415L;  
    public void execute () {
        Date date=new Date();  
        System.out.println(date.toLocaleString());  
    }
    
}

3.4 配置quartz.xml文件

<bean id="Test" class="org.lxh.mvc.job.Test" scope="prototype">
    </bean>

    <bean id="TestjobTask" class="org.springframework.scheduling.quartz.JobDetailBean">
        <property name="jobClass">
            <value>org.lxh.mvc.jobbean.AutohomeDetailQuartzJobBean</value>
        </property>
        <property name="jobDataAsMap">
            <map>
                <entry key="targetObject" value="Test" />
                <entry key="targetMethod" value="execute" />
             </map>
         </property> 
     </bean>
    
    <bean name="TestTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">  
        <property name="jobDetail" ref="TestjobTask" />
        <property name="cronExpression" value="0/1 * * * * ?" />
    </bean> 
    
<bean id="quartzScheduler"
    class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="configLocation" value="classpath:quartz.properties"/>
        <property name="triggers">
            <list>
                <ref bean="TestTrigger" />
            </list>
        </property>
        <property name="applicationContextSchedulerContextKey" value="applicationContext" />
    </bean>

3.5 測試

  ServerA、ServerB的代碼、配置徹底同樣,先啓動ServerA,後啓動ServerB,當Server關斷以後,ServerB會監測到其關閉,並將ServerA上正在執行的Job接管,繼續執行。

四、Quartz集羣實例(單獨Quartz)

  儘管咱們已經實現了Spring+Quartz的集羣配置,可是由於Spring與Quartz之間的兼容問題仍是不建議使用該方式。在本小節中,咱們實現了單獨用Quartz配置的集羣,相對Spring+Quartz的方式來講,簡單、穩定。

4.1 工程結構

  咱們採用單獨使用Quartz來實現其集羣功能,代碼結構及所需的第三方jar包如圖3.1所示。其中,Mysql版本:5.1.52,Mysql驅動版本:mysql-connector-java-5.1.5-bin.jar(針對於5.1.52,建議採用該版本驅動,由於Quartz存在BUG使得其與某些Mysql驅動結合時不能正常運行)。

 

圖4.1 Quartz集羣工程結構及所需第三方jar包

  其中quartz.properties爲Quartz配置文件,放在src目錄下,若無該文件,Quartz將自動加載jar包中的quartz.properties文件;SimpleRecoveryJob.java、SimpleRecoveryStatefulJob.java爲兩個Job;ClusterExample.java中編寫了調度信息、觸發機制及相應的測試main函數。

4.2 配置文件quartz.properties

  默認文件名稱quartz.properties,經過設置"org.quartz.jobStore.isClustered"屬性爲"true"來激活集羣特性。在集羣中的每個實例都必須有一個惟一的"instance id" ("org.quartz.scheduler.instanceId" 屬性), 可是應該有相同的"scheduler instance name" ("org.quartz.scheduler.instanceName"),也就是說集羣中的每個實例都必須使用相同的quartz.properties 配置文件。除了如下幾種例外,配置文件的內容其餘都必須相同:

  a.線程池大小。

  b.不一樣的"org.quartz.scheduler.instanceId"屬性值(經過設定爲"AUTO"便可)。

複製代碼

#==============================================================  
#Configure Main Scheduler Properties  
#==============================================================   
org.quartz.scheduler.instanceName = quartzScheduler
org.quartz.scheduler.instanceId = AUTO

#==============================================================  
#Configure JobStore  
#============================================================== 
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 10000  
org.quartz.jobStore.dataSource = myDS
 
#==============================================================  
#Configure DataSource  
#============================================================== 
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://192.168.31.18:3306/test?useUnicode=true&amp;characterEncoding=UTF-8
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = 123456
org.quartz.dataSource.myDS.maxConnections = 30

#==============================================================  
#Configure ThreadPool  
#============================================================== 
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

五、注意事項

5.1 時間同步問題

  Quartz實際並不關心你是在相同仍是不一樣的機器上運行節點。當集羣放置在不一樣的機器上時,稱之爲水平集羣。節點跑在同一臺機器上時,稱之爲垂直集羣。對於垂直集羣,存在着單點故障的問題。這對高可用性的應用來講是沒法接受的,由於一旦機器崩潰了,全部的節點也就被終止了。對於水平集羣,存在着時間同步問題。

  節點用時間戳來通知其餘實例它本身的最後檢入時間。假如節點的時鐘被設置爲未來的時間,那麼運行中的Scheduler將再也意識不到那個結點已經宕掉了。另外一方面,若是某個節點的時鐘被設置爲過去的時間,也許另外一節點就會認定那個節點已宕掉並試圖接過它的Job重運行。最簡單的同步計算機時鐘的方式是使用某一個Internet時間服務器(Internet Time Server ITS)。

5.2 節點爭搶Job問題

  由於Quartz使用了一個隨機的負載均衡算法, Job以隨機的方式由不一樣的實例執行。Quartz官網上提到當前,還不存在一個方法來指派(釘住) 一個 Job 到集羣中特定的節點。

5.3 從集羣獲取Job列表問題

  當前,若是不直接進到數據庫查詢的話,尚未一個簡單的方式來獲得集羣中全部正在執行的Job列表。請求一個Scheduler實例,將只能獲得在那個實例上正運行Job的列表。Quartz官網建議能夠經過寫一些訪問數據庫JDBC代碼來從相應的表中獲取所有的Job信息。

 

我按照上述步驟搭建好集羣后,因爲沒有注意到AutohomeDetailQuartzJobDetail中的

m = otargetObject.getClass().getMethod(targetMethod, new Class[] {JobExecutionContext.class});

這段代碼,由於個人集羣定時任務是從普通的定時任務改版成的,因爲之前的方法沒有參數,故每次定時任務時間到了,執行時,老是不進個人方法,最後發現是參數和上述代碼中的參數不一致形成的。囧

下一步計劃作quartz監控,發郵件,發短信啥的。

相關文章
相關標籤/搜索