1. 程式人生 > >集群環境下定時調度的解決方案之Quartz集群

集群環境下定時調度的解決方案之Quartz集群

water err eas ask res article 解決問題 class lis

集群環境可能出現的問題

在上一篇博客我們介紹了如何在自己的項目中從無到有的添加了Quartz定時調度引擎,其實就是一個Quartz 和Spring的整合過程,很容易實現,但是我們現在企業中項目通常都是部署在集群環境中的,這樣我們之前的定時調度就會出現問題了,因為我們的定時任務都加載在內存中的,每個集群節點中的調度器都會去執行,這就會存在重復執行和資源競爭的問題,那麽如何來解決這樣的問題呢,往下面看吧...

解決方案

在一般的企業中解決類似的問題一般都是在一個note上部署Quartz其他note不部署(或者是在其他幾個機器加IP地址過濾),但是這樣集群對於定時任務來說就沒有什麽意義了,而且存在著單點故障的隱患,也就是這臺部署著Quartz的機器一旦掛了,我們的定時任務就停止服務了,這絕對不是我們想要的。

Quartz本身是支持集群的,我們通過Quartz的集群方式來解決這樣的問題。

Quartz集群

技術分享圖片

雖然單個 Quartz 實例能給予你很好的 Job調度能力,但它不能令典型的企業需求,如可伸縮性、高可靠性滿足。假如你需要故障轉移的能力並能運行日益增多的 Job,Quartz 集群勢必成為你方言的一部分了,並且即使是其中一臺機器在最糟的時間崩潰了也能確保所有的 Job 得到執行。 QuartzJob Scheduling Framework

了解了Quartz集群的好處,接下來就對我們之前的工程進行改造,增加Quartz集群特性。

Quartz集群中節點依賴於數據庫來傳播 Scheduler 實例的狀態,你只能在使用 JDBC JobStore 時應用 Quartz 集群

所以我們集群的第一步就是建立Quartz所需要的12張表:

1、建表

在quartz核心包裏面通過quartz提供的建表語句建立相關表結構

技術分享圖片

生成的表結構如下

技術分享圖片

這幾張表是用於存儲任務信息,觸發器,調度器,集群節點等信息

詳細解釋:
QRTZ_CRON_TRIGGERS 存儲Cron Trigger,包括Cron 表達式和時區信息
QRTZ_PAUSED_TRIGGER_GRPS 存儲已暫停的Trigger 組的信息
QRTZ_LOCKS 存儲程序的非觀鎖的信息(假如使用了悲觀鎖)
QRTZ_JOB_LISTENERS 存儲有關已配置的JobListener 的信息
QRTZ_BLOG_TRIGGERS Trigger 作為Blob 類型存儲(用於Quartz 用戶用JDBC 創建他們自己定制的Trigger 類型,JobStore並不知道如何存儲實例的時候)
QRTZ_TRIGGERS 存儲已配置的Trigger 的信息
所有的表默認以前綴QRTZ_開始。可以通過在quartz.properties配置修改(org.quartz.jobStore.tablePrefix= QRTZ_)。

2、編寫quartz.properties文件

建立 quartz.properties文件把它放在工程的 src 目錄下,內容如下:

技術分享圖片
  1 #============================================================================
  2 
  3 # Configure Main Scheduler Properties 
  4 
  5 #============================================================================
  6 
  7  
  8 
  9 org.quartz.scheduler.instanceName = Mscheduler
 10 
 11 org.quartz.scheduler.instanceId = AUTO
 12 
 13 org.quartz.jobStore.clusterCheckinInterval=20000
 14 
 15  
 16 
 17 #============================================================================
 18 
 19 # Configure ThreadPool 
 20 
 21 #============================================================================
 22 
 23  
 24 
 25 org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
 26 
 27 org.quartz.threadPool.threadCount = 3
 28 
 29 org.quartz.threadPool.threadPriority = 5
 30 
 31  
 32 
 33 #============================================================================
 34 
 35 # Configure JobStore 
 36 
 37 #============================================================================
 38 
 39  
 40 
 41 #org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
 42 
 43  
 44 
 45 org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
 46 
 47 #org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
 48 
 49 org.quartz.jobStore.useProperties = true
 50 
 51 #org.quartz.jobStore.dataSource = myDS
 52 
 53 org.quartz.jobStore.tablePrefix = QRTZ_
 54 
 55 org.quartz.jobStore.isClustered = true
 56 
 57 org.quartz.jobStore.maxMisfiresToHandleAtATime=1
 58 
 59 #============================================================================
 60 
 61 # Configure Datasources 
 62 
 63 #============================================================================
 64 
 65  
 66 
 67 #mysql
 68 
 69 #org.quartz.dataSource.myDS.driver = com.ibm.db2.jcc.DB2Driver
 70 
 71 #org.quartz.dataSource.myDS.URL = jdbc:db2://localhost:50000/db
 72 
 73 #org.quartz.dataSource.myDS.user = db2
 74 
 75 #org.quartz.dataSource.myDS.password = db2
 76 
 77 #org.quartz.dataSource.myDS.maxConnections = 5
 78 
 79  
 80 
 81 #oracle
 82 
 83 #org.quartz.dataSource.myDS.driver = oracle.jdbc.driver.OracleDriver
 84 
 85 #org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@localhost:1521:orcl
 86 
 87 #org.quartz.dataSource.myDS.user = scott
 88 
 89 #org.quartz.dataSource.myDS.password = shao
 90 
 91 #org.quartz.dataSource.myDS.maxConnections = 5
 92 
 93  
 94 
 95 #For Tomcat
 96 
 97 org.quartz.jobStore.driverDelegateClass =org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
 98 
 99 #For Weblogic & Websphere
100 
101 #org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.WebLogicDelegate
102 
103 org.quartz.jobStore.useProperties = false
104 
105 org.quartz.jobStore.dataSource = myDS
106 
107  
108 
109  
110 
111 #JNDI MODE
112 
113 #For Tomcat
114 
115 org.quartz.dataSource.myDS.jndiURL=java:comp/env/jdbc/oracle
116 
117 #For Weblogic & Websphere
118 
119 #org.quartz.dataSource.myDS.jndiURL=jdbc/oracle
120 
121  
122 
123  
124 
125 #============================================================================
126 
127 # Configure Plugins
128 
129 #============================================================================
130 
131  
132 
133 #org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingJobHistoryPlugin
134 
135  
136 
137 #org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin
138 
139 #org.quartz.plugin.jobInitializer.fileNames = jobs.xml
140 
141 #org.quartz.plugin.jobInitializer.overWriteExistingJobs = true
142 
143 #org.quartz.plugin.jobInitializer.failOnFileNotFound = true
144 
145 #org.quartz.plugin.jobInitializer.scanInterval = 10
146 
147 #org.quartz.plugin.jobInitializer.wrapInUserTransaction = false 
技術分享圖片

紅色加粗部分是集群需要的配置

核心配置解釋如下:

org.quartz.jobStore.class 屬性為JobStoreTX,
將任務持久化到數據中。因為集群中節點依賴於數據庫來傳播Scheduler實例的狀態,你只能在使用JDBC JobStore 時應用Quartz 集群。

org.quartz.jobStore.isClustered 屬性為true,通知Scheduler實例要它參與到一個集群當中。

org.quartz.jobStore.clusterCheckinInterval

屬性定義了Scheduler實例檢入到數據庫中的頻率(單位:毫秒)。
Scheduler 檢查是否其他的實例到了它們應當檢入的時候未檢入;
這能指出一個失敗的Scheduler 實例,且當前Scheduler 會以此來接管任何執行失敗並可恢復的Job。
通過檢入操作,Scheduler也會更新自身的狀態記錄。clusterChedkinInterval越小,Scheduler節點檢查失敗的Scheduler 實例就越頻繁。默認值是 20000 (即20 秒)

3、修改spring-time.xml文件

技術分享圖片
 1 <?xmlversion="1.0"encoding="UTF-8"?>
 2 <!DOCTYPEbeansPUBLIC"-//SPRING//DTD BEAN//EN"
 3         "http://www.springframework.org/dtd/spring-beans.dtd">
 4 <beans>
 5  
 6      <!-- 調度器lazy-init=‘false‘那麽容器啟動就會執行調度程序 -->
 7      <beanid="startQuertz"lazy-init="false"autowire="no"class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
 8         <property name="dataSource" ref="dataSource"/>
 9         <property name="configLocation" value="classpath:quartz.properties" />
10          <propertyname="triggers">
11               <list>
12                    <refbean="doTime"/>
13               </list>
14          </property>
15          <!-- 允許在Quartz上下文中使用Spring實例工廠 -->
16          <propertyname="applicationContextSchedulerContextKey"value="applicationContext"/>
17      </bean>
18     
19      <!-- 觸發器 -->
20      <beanid="doTime"class="org.springframework.scheduling.quartz.CronTriggerBean">
21          <propertyname="jobDetail"ref="jobtask"></property>
22          <!-- cron表達式 -->
23          <propertyname="cronExpression"value="10,15,20,25,30,35,40,45,50,55 * * * * ?"></property>
24      </bean>
25     
26      <!-- 任務 -->
27      <beanid="jobtask"class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
28          <propertyname="targetObject"ref="synUsersJob"></property>
29          <propertyname="targetMethod"value="execute"></property>
30      </bean>
31     
32      <!-- 要調用的工作類 -->
33      <beanid="synUsersJob"class="org.leopard.core.quartz.job.SynUsersJob"></bean>
34       
35 </beans>
技術分享圖片

增加紅色加粗部分代碼,註入數據源和加載quartz.properties文件

OK Quartz集群的配置只有這幾步,我們來啟動項目。。。

我們啟著啟著….報錯了!

17:00:59,718 ERROR ContextLoader:215 - Context initialization failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘startQuertz‘ defined in class path resource [config/spring/spring-time.xml]: Invocation of init method failed; nested exception is org.quartz.JobPersistenceException:Couldn‘t store job: Unable to serialize JobDataMap for insertion into database because the value of property ‘methodInvoker‘ is not serializable: org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean [See nested exception: java.io.NotSerializableException: Unable to serialize JobDataMap for insertion into database because the value of property ‘methodInvoker‘ is not serializable: org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean]

我們主要來看紅色部分,主要原因就是這個MethodInvokingJobDetailFactoryBean 類中的 methodInvoking 方法,是不支持序列化的,因此在把 quartz 的 task 序列化進入數據庫時就會拋這個serializable的錯誤

4、解決serializable錯誤解決方案

網上查了一下,解決這個問題,目前主要有兩種方案:

4.1.修改Spring的源碼

博客地址:http://jira.springframework.org/browse/SPR-3797

作者重寫了MethodInvokingJobDetailFactoryBean

4.2.通過AOP反射對Spring源碼進行切面重構

博客地址:http://blog.csdn.net/lifetragedy/article/details/6212831

根據 QuartzJobBean 來重寫一個自己的類,然後使用 SPRING 把這個重寫的類(我們就名命它為: MyDetailQuartzJobBean )註入 appContext 中後,再使用 AOP 技術反射出原有的 quartzJobx( 就是開發人員原來已經做好的用於執行 QUARTZ 的 JOB 的執行類 ) 。

兩種方式我都進行了測試,都可以解決問題,我們這裏先通過第二種方式解決這個bug,沒有修改任何Spring的源碼

4.2.1、增加MyDetailQuartzJobBean.Java

技術分享圖片
 1 package org.leopard.core.quartz;
 2  
 3 import java.lang.reflect.Method;
 4  
 5 import org.apache.commons.logging.Log;
 6 import org.apache.commons.logging.LogFactory;
 7 import org.quartz.JobExecutionContext;
 8 import org.quartz.JobExecutionException;
 9 import org.springframework.context.ApplicationContext;
10 import org.springframework.scheduling.quartz.QuartzJobBean;
11  
12 /**
13  * 解決Spring和Quartz整合bug
14  *
15  */
16 public class MyDetailQuartzJobBean extends QuartzJobBean {
17          protected final Log logger = LogFactory.getLog(getClass());
18  
19          private String targetObject;
20          private String targetMethod;
21          private ApplicationContext ctx;
22  
23          protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
24                    try {
25  
26                             logger.info("execute [" + targetObject + "] at once>>>>>>");
27                             Object otargetObject = ctx.getBean(targetObject);
28                             Method m = null;
29                             try {
30                                      m = otargetObject.getClass().getMethod(targetMethod, new Class[] {});
31  
32                                      m.invoke(otargetObject, new Object[] {});
33                             } catch (SecurityException e) {
34                                      logger.error(e);
35                             } catch (NoSuchMethodException e) {
36                                      logger.error(e);
37                             }
38  
39                    } catch (Exception e) {
40                             throw new JobExecutionException(e);
41                    }
42  
43          }
44  
45          public void setApplicationContext(ApplicationContext applicationContext) {
46                    this.ctx = applicationContext;
47          }
48  
49          public void setTargetObject(String targetObject) {
50                    this.targetObject = targetObject;
51          }
52  
53          public void setTargetMethod(String targetMethod) {
54                    this.targetMethod = targetMethod;
55          }
56  
57 }
技術分享圖片

5、再次修改spring-time.xml文件解決serializable問題

修改後的spring-time.xml文件內容如下:

技術分享圖片
 1 <?xmlversion="1.0"encoding="UTF-8"?>
 2 <!DOCTYPEbeansPUBLIC"-//SPRING//DTD BEAN//EN"
 3         "http://www.springframework.org/dtd/spring-beans.dtd">
 4 <beans>
 5  
 6     <!-- 調度器lazy-init=‘false‘那麽容器啟動就會執行調度程序 -->
 7     <beanid="startQuertz"lazy-init="false"autowire="no"class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
 8         <propertyname="dataSource"ref="dataSource"/>
 9         <propertyname="configLocation"value="classpath:quartz.properties"/>
10         <propertyname="triggers">
11             <list>
12                 <refbean="doTime"/>
13             </list>
14         </property>
15         <!--這個是必須的,QuartzScheduler延時啟動,應用啟動完後 QuartzScheduler再啟動-->
16         <propertyname="startupDelay"value="30"/>     
17         <!--這個是可選,QuartzScheduler啟動時更新己存在的Job,這樣就不用每次修改targetObject後刪除qrtz_job_details表對應記錄了-->
18         <propertyname="overwriteExistingJobs"value="true"/>
19         <!-- 允許在Quartz上下文中使用Spring實例工廠 -->
20         <propertyname="applicationContextSchedulerContextKey"value="applicationContext"/>
21     </bean>
22    
23     <!-- 觸發器 -->
24     <beanid="doTime"class="org.springframework.scheduling.quartz.CronTriggerBean">
25         <propertyname="jobDetail"ref="jobtask"></property>
26         <!-- cron表達式 -->
27         <propertyname="cronExpression"value="10,15,20,25,30,35,40,45,50,55 * * * * ?"></property>
28     </bean>
29    
30     <!-- 任務 -->
31     <beanid="jobtask"class="org.springframework.scheduling.quartz.JobDetailBean">
32         <propertyname="jobClass">
33              <value>org.leopard.core.quartz.MyDetailQuartzJobBean</value>
34         </property>
35         <propertyname="jobDataAsMap">
36             <map>
37                   <entrykey="targetObject"value="synUsersJob"/>
38                   <entrykey="targetMethod"value="execute"/>
39             </map>
40         </property>
41     </bean>
42    
43     <!-- 要調用的工作類 -->
44     <beanid="synUsersJob"class="org.leopard.core.quartz.job.SynUsersJob"></bean>
45      
46 </beans> 
技術分享圖片

主要看紅色加粗部分...

測試

Ok 配置完成,我們把oa_ssh部署到兩臺tomcat上面,分別啟動。

技術分享圖片

可以看到我們先啟動的tomcat控制臺打印出日誌

技術分享圖片

另外一臺沒有打印日誌

技術分享圖片

這時我們把執行定時任務的那臺tomcat停止,可以看到等了一會之後,我們的另外一臺tomcat會把之前tomcat執行的定時任務接管過來繼續執行,我們的集群是成功的。

原創文章

集群環境下定時調度的解決方案之Quartz集群