一、定時任務的必要性:
定時任務在應用中的重要性不言而喻,大可能是應用,特別是金融應用更是離不開定時任務,能用定時任務來處理異常訂單,完成跑批,定時活動(雙11)等。
在初期應用的訪問量並非那麼大,一臺服務器徹底知足使用,可是隨着用戶量、業務量的逐日增長,應用中會有不少定時任務須要執行,一臺服務器已經不能知足使用,
所以須要把應用給部署到集羣中,前端經過nginx代理實現訪問。
二、集羣使用定時任務的問題:
目前大部分在集羣中處理定時任務的方式不是正真的分佈式處理方式,而是一種僞分佈式,這種方式存在一個明顯的缺陷就是當集羣中機器宕機,
那麼整個定時任務就會掛掉或者不能一次性跑完,會對業務產生嚴重的影響。
並且在集羣環境中,一樣的定時任務,在集羣中的每臺服務器都會執行,這樣定時任務就會重複執行,不但會增長服務器的負擔,還會由於定時任務重複執行形成額外的不可預期的錯誤。
解決方案是:
根據集羣的數量,把定時任務中的任務平均分到集羣中的每臺機器上(這裏的平均分是指之前多個定時任務原本是在一臺機器上運行,先在人爲的把這些任務分紅幾部分,讓全部的機器分別去執行這些任務)
這就是採用了分佈式定時任務來進行處理。
另一種解決方式:
使用Quartz框架,在集羣環境下,經過數據庫鎖機制來實現定時任務的執行,下面會介紹。
三、Quartz介紹:
Quartz是一個開放源碼項目,專一於任務調度器,提供了極爲普遍的特性如持久化任務,集羣和分佈式任務等。
Quartz核心是調度器,還採用多線程管理。quartz框架是原生就支持分佈式定時任務的。
1.持久化任務(把調度信息存儲到數據):當應用程序中止運行時,全部調度信息不被丟失,當你從新啓動時,調度信息還存在,這就是持久化任務。
2.集羣和分佈式處理:當在集羣環境下,當有配置Quartz的多個客戶端時(節點),
採用Quartz的集羣和分佈式處理時,咱們要了解幾點好處
1) 一個節點沒法完成的任務,會被集羣中擁有相同的任務的節點取代執行。
2) Quartz調度是經過觸發器的類別來識別不一樣的任務,在不一樣的節點定義相同的觸發器的類別,這樣在集羣下能穩定的運行,一個節點沒法完成的任務,會被集羣中擁有相同的任務的節點取代執行。
3)分佈式 體如今 當相同的任務定時在一個時間點,在那個時間點,不會被兩個節點同時執行。
四、Quartz 在集羣如何工做:
一個 Quartz 集羣中的每一個節點是一個獨立的 Quartz 應用,你必須對每一個節點分別啓動或中止。
大多數應用服務器的集羣,獨立的 Quartz 節點並不與另外一其的節點或是管理節點通訊,彼此相互獨立。
Quartz 應用是經過數據庫表來感知到另外一應用的,調度信息存儲在數據庫中,當集羣定時任務操做數據庫(讀取任務信息,更新任務信息)
數據庫就會被加鎖,防止其餘相同的任務也讀取到該任務,避免任務的重複執行。
五、環境搭建-建立Quartz數據庫表
由於Quartz 集羣依賴於數據庫,因此必須首先建立Quartz數據庫表。
Quartz 包括了全部被支持的數據庫平臺的 SQL 腳本。在 <quartz_home>/docs/dbTables 目錄下找到那些 SQL 腳本,這裏的 <quartz_home> 是解壓 Quartz 分發包後的目錄。
Quartz 2.2.3版本,總共11張表,不一樣版本,表個數可能不一樣。數據庫爲mysql,用tables_mysql_innodb.sql建立數據庫表,數據庫不一樣選擇的建表sql也不一樣,根據本身選擇。
六、環境搭建-配置 Quartz 使用集羣
1.配置節點的 quartz.properties 文件
# Configure Main Scheduler Properties
# Needed to manage cluster instances
org.quartz.scheduler.instanceName = TestScheduler1
org.quartz.scheduler.instanceId = AUTO
# Configure ThreadPool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
# Configure JobStore
# Using Spring datasource in quartzJobsConfig.xml
# Spring uses LocalDataSourceJobStore extension of JobStoreCMT
org.quartz.jobStore.misfireThreshold = 60000
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.maxMisfiresToHandleAtATime=10
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
org.quartz.scheduler.instanceName:
屬性可爲任何值,用在 JDBC JobStore 中來惟一標識實例,可是全部集羣節點中必須相同。
org.quartz.scheduler.instanceId:
屬性爲 AUTO便可,基於主機名和時間戳來產生實例 ID。
org.quartz.jobStore.class:
屬性爲 JobStoreTX,將任務持久化到數據中。由於集羣中節點依賴於數據庫來傳播 Scheduler 實例的狀態,你只能在使用 JDBC JobStore 時應用 Quartz 集羣。
這意味着你必須使用 JobStoreTX 或是 JobStoreCMT 做爲 Job 存儲;你不能在集羣中使用 RAMJobStore。
org.quartz.jobStore.isClustered:
屬性爲 true,你就告訴了 Scheduler 實例要它參與到一個集羣當中。
這一屬性會貫穿於調度框架的始終,用於修改集羣環境中操做的默認行爲。
org.quartz.jobStore.clusterCheckinInterval:
屬性定義了Scheduler 實例檢入到數據庫中的頻率(單位:毫秒)。
Scheduler 檢查是否其餘的實例到了它們應當檢入的時候未檢入;這能指出一個失敗的 Scheduler 實例,且當前 Scheduler 會以此來接管任何執行失敗並可恢復的 Job。
經過檢入操做,Scheduler 也會更新自身的狀態記錄。clusterChedkinInterval 越小,Scheduler 節點檢查失敗的 Scheduler 實例就越頻繁。默認值是 15000 (即15 秒)。
七、配置applicationContext-quartz.xml文件,在這裏配置任務,數據庫鏈接等
- <span style="font-size:14px;"><beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" </span>
- <span style="font-size:14px;"><span style="white-space:pre"> </span>xmlns:context="http://www.springframework.org/schema/context"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-4.0.xsd </span>
applicationContextSchedulerContextKey:
是org.springframework.scheduling.quartz.SchedulerFactoryBean這個類中把spring上下文以key/value的方式存放在了SchedulerContext中了,
能夠用applicationContextSchedulerContextKey所定義的key獲得對應spring 的ApplicationContext;
會在後面的類裏用到
configLocation:用於指明quartz的配置文件的位置
requestsRecovery:
屬性必須設置爲 true,當Quartz服務被停止後,再次啓動或集羣中其餘機器接手任務時會嘗試恢復執行以前未完成的全部任務。
八、介紹相關的類:
一、html
- <property name="jobFactory">
- <bean
- class="com.clusterquartz.autowired.AutowiringSpringBeanJobFactory" />
- </property>
這個類的做用:前端
- <span style="font-size:14px;">
- public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory
- implements ApplicationContextAware {
- private transient AutowireCapableBeanFactory beanFactory;
-
- public void setApplicationContext(ApplicationContext applicationContext)
- throws BeansException {
- beanFactory = applicationContext.getAutowireCapableBeanFactory();
- }
-
- @Override
- protected Object createJobInstance(TriggerFiredBundle bundle)
- throws Exception {
- Object job = super.createJobInstance(bundle);
- beanFactory.autowireBean(job);
- return job;
- }
- }</span>
調用咱們任務類的實現:
/**
* @author
* @description 當前任務是每隔必定時間打印當前的時間
*/
public class PrintCurrentTimeJobs extends QuartzJobBean {
private static final Log LOG_RECORD = LogFactory
.getLog(PrintCurrentTimeJobs.class);
/*
* 這裏就是由於有上文中的AutowiringSpringBeanJobFactory纔可使用像@Autowired的註解,固然還可使用Spring的其餘註解
* 不然只能在配置文件中設置這屬性的值,另外一種方式下面說到
*/
@Autowired
private ClusterQuartz clusterQuartz;
protected void executeInternal(JobExecutionContext jobExecutionContext)
throws JobExecutionException {
LOG_RECORD.info("begin to execute task,"
+ DateUtil.dateFmtToString(new Date()));
//咱們真正要執行的任務
clusterQuartz.printUserInfo();
LOG_RECORD.info("end to execute task,"
+ DateUtil.dateFmtToString(new Date()));
}
}
咱們定時任務的具體邏輯實現:(咱們很熟悉的Spring,註解開發)
@Service("ClusterQuartz")
public class ClusterQuartz {
private static final Logger logger = LoggerFactory
.getLogger(ClusterQuartz.class);
/*在這裏可使用spring的註解,引入各類服務之類的*/
/*@Resource(name = "miService")
private MiService miService;
@Autowired
private PushServiceI pushRecordService;*/
public void printUserInfo() {
System.out.println("定時任務的實現邏輯代碼");
}
}
二、若是不配置上面1的那個property,咱們的定時任務這樣實現:
- <span style="white-space:pre"> </span><span style="font-size:14px;">
- @PersistJobDataAfterExecution
- @DisallowConcurrentExecution
-
- public class MyQuartzJobBean1 extends QuartzJobBean {
-
-
- private static final Logger logger = LoggerFactory
- .getLogger(MyQuartzJobBean1.class);
-
-
- @Override
- protected void executeInternal(JobExecutionContext jobexecutioncontext)
- throws JobExecutionException {
-
-
- SimpleService simpleService = getApplicationContext(jobexecutioncontext)
- .getBean("simpleService", SimpleService.class);
-
- simpleService.testMethod1();
- }
-
-
- private ApplicationContext getApplicationContext(
- final JobExecutionContext jobexecutioncontext) {
- try {
-
-
- return (ApplicationContext) jobexecutioncontext.getScheduler()
- .getContext().get("applicationContextKey");
- } catch (SchedulerException e) {
- logger.error(
- "jobexecutioncontext.getScheduler().getContext() error!", e);
- throw new RuntimeException(e);
- }
- }
- }
-
-
- 而定時任務的具體邏輯業務,仍是和上面的同樣。
- @Service("simpleService")
- public class SimpleService {
-
- private static final Logger logger = LoggerFactory.getLogger(SimpleService.class);
-
- public void testMethod1(){
-
- logger.info("testMethod1.......1");
- System.out.println("2--testMethod1......."+System.currentTimeMillis()/1000);
- }
-
- public void testMethod2(){
- logger.info("testMethod2.......2");
- }
- }</span>
九、運行測試Quartz集羣定時任務:
public class MainTest {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext(
new String[] { "classpath:applicationContext.xml",
"classpath:applicationContext-quartz.xml" });
}
}
十、在Spring中使用Quartz有兩種方式實現:
第一種是任務類繼承QuartzJobBean,第二種則是在配置文件裏定義任務類和要執行的方法,類和方法仍然是普通類。
很顯然,第二種方式遠比第一種方式來的靈活。咱們發現上面採用的就是第一種方法,下面說下第二種方法。
一、配置XML以下:
<!-- 方式二:使用MethodInvokingJobDetailFactoryBean,任務類能夠不實現Job接口,經過targetMethod指定調用方法 -->
<!-- 定義目標bean和bean中的方法 -->
<bean id="SpringQtzJob" class="com.clusterquartz.job.SpringQtz" />
<bean id="SpringQtzJobMethod"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject">
<ref bean="SpringQtzJob" />
</property>
<property name="targetMethod"> <!-- 要執行的方法名稱 -->
<value>execute</value>
</property>
</bean>
<!-- 調度觸發器 -->
<bean name="printCurrentTimeScheduler"
class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="SpringQtzJobMethod" />
<property name="cronExpression">
<value>0/10 * * * * ?</value>
</property>
</bean>
二、任務類:(普通的Java類)
public class SpringQtz {
private static int counter = 0;
protected void execute() {
long ms = System.currentTimeMillis();
System.out.println("\t\t" + new Date(ms));
System.out.println("(" + counter++ + ")");
}
}
十一、Quartz版本問題:
spring3.1如下的版本必須使用quartz1.x系列,3.1以上的版本才支持quartz 2.x,否則會出錯。
不匹配異常:
Caused by: org.springframework.beans.factory.CannotLoadBeanClassException: Error loading class [org.springframework.scheduling.quartz.CronTriggerBean] for bean with name 'mytrigger' defined in class path resource [applicationContext.xml]: problem with class file or dependent class; nested exception is java.lang.IncompatibleClassChangeError: class org.springframework.scheduling.quartz.CronTriggerBean has interface org.quartz.CronTrigger as super class
異常緣由:
spring3.0.5中org.springframework.scheduling.quartz.CronTriggerBean繼承了org.quartz.CronTrigger(public class CronTriggerBeanextends CronTrigger),
而在quartz2.1.3中org.quartz.CronTrigger是個接口(publicabstract interface CronTrigger extends Trigger),
而在quartz1.8.5及1.8.4中org.quartz.CronTrigger是個類(publicclass CronTrigger extends Trigger),
從而形成沒法在applicationContext中配置觸發器。這是spring3.1如下版本和quartz2版本不兼容的一個bug。
十二、關於cronExpression表達式,這裏提一下:
字段 容許值 容許的特殊字符
秒 0-59 , - * /
分 0-59 , - * /
小時 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 或者 JAN-DEC , - * /
星期 1-7 或者 SUN-SAT , - * ? / L C #
年(可選) 留空, 1970-2099 , - * /
表達式意義
"0 0 12 * * ?" 天天中午12點觸發
"0 15 10 ? * *" 天天上午10:15觸發
"0 15 10 * * ?" 天天上午10:15觸發
"0 15 10 * * ? *" 天天上午10:15觸發
"0 15 10 * * ? 2005" 2005年的天天上午10:15觸發
"0 * 14 * * ?" 在天天下午2點到下午2:59期間的每1分鐘觸發
"0 0/5 14 * * ?" 在天天下午2點到下午2:55期間的每5分鐘觸發
"0 0/5 14,18 * * ?" 在天天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
"0 0-5 14 * * ?" 在天天下午2點到下午2:05期間的每1分鐘觸發
"0 10,44 14 ? 3 WED" 每一年三月的星期三的下午2:10和2: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 2002-2005" 2002年至2005年的每個月的最後一個星期五上午10:15觸發
"0 15 10 ? * 6#3" 每個月的第三個星期五上午10:15觸發
天天早上6點
0 6 * * *
每兩個小時
0 */2 * * *
晚上11點到早上8點之間每兩個小時,早上八點
0 23-7/2,8 * * *
每月的4號和每一個禮拜的禮拜一到禮拜三的早上11點
0 11 4 * 1-3
1月1日早上4點
0 4 1 1 *
1三、通常applicationContext-quartz.xml的框架配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 任務調度配置 -->
<bean name="quartzScheduler"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<!-- 配置數據庫 -->
<property name="dataSource">
<ref bean="dataSource" />
</property>
<property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
<property name="configLocation" value="classpath:quartz.properties" />
<property name="triggers">
<!-- 相似的能夠在這裏添加定時任務觸發器 -->
<list>
<ref bean="trigger1" />
<ref bean="trigger2" />
</list>
</property>
</bean>
<!-- 任務具體工廠 -->
<bean id="jobDetail1"
class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<!-- 具體實現類,固然這裏咱們的class繼承 QuartzJobBean了 -->
<property name="jobClass">
<value>com.sundoctor.quartz.cluster.example.MyQuartzJobBean1</value>
</property>
<property name="durability" value="true" />
<property name="requestsRecovery" value="true" />
</bean>
<!-- 配置觸發器,包含執行時間,具體任務等 -->
<bean id="trigger1"
class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail1" />
<property name="cronExpression" value="0/10 * * * * ?" />
</bean>
</beans>
1四、Quartz運行解釋:
Quartz 實際並不關心你是在相同的仍是不一樣的機器上運行節點。
當集羣是放置在不一樣的機器上時,一般稱之爲水平集羣。節點是跑在同一臺機器是,稱之爲垂直集羣。
對於垂直集羣,存在着單點故障的問題。這對高可用性的應用來講是個壞消息,由於一旦機器崩潰了,全部的節點也就被有效的終止了。
當你運行水平集羣時,時鐘應當要同步,以避免出現離奇且不可預知的行爲。
假如時鐘沒可以同步,Scheduler 實例將對其餘節點的狀態產生混亂。
有幾種簡單的方法來保證時鐘何持同步,並且也沒有理由不這麼作。最簡單的同步計算機時鐘的方式是使用某一個 Internet 時間服務器(Internet Time Server ITS)。
沒什麼會阻止你在相同環境中使用集羣的和非集羣的 Quartz 應用。
惟一要注意的是這兩個環境不要混用在相同的數據庫表。
意思是非集羣環境不要使用與集羣應用相同的一套數據庫表;不然將獲得希奇古怪的結果,集羣和非集羣的 Job 都會遇到問題。
假如你讓一個非集羣的 Quartz 應用與集羣節點並行着運行,設法使用 JobInitializationPlugin和 RAMJobStore。
1五、配置數據庫鏈接,
一、通用的
<!-- 數據源定義,使用c3p0 鏈接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass" value="${jdbc.driverClassName}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="initialPoolSize" value="2" />
<property name="minPoolSize" value="10" />
<property name="maxPoolSize" value="20" />
<property name="acquireIncrement" value="2" />
<property name="maxIdleTime" value="1800" />
</bean>
二、JNDI鏈接數據庫
<!-- JNDI鏈接數據庫 -->
<bean name="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<!-- tomcat -->
<!--<property name="jndiName" value="java:comp/env/jndi/mysql/quartz"/> -->
<!-- jboss -->
<property name="jndiName" value="jdbc/quartz" />
</bean>
三、Druid鏈接池(公司用)
<bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url" value="${jdbc_url}" />
<property name="username" value="${jdbc_username}" />
<property name="password" value="${jdbc_password}" />
<property name="initialSize" value="0" />
<property name="maxActive" value="20" />
<property name="maxIdle" value="50" />
<property name="minIdle" value="0" />
<property name="maxWait" value="60000" />
<property name="validationQuery" value="${validationQuery}" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" />
<!-- 打開removeAbandoned功能 -->
<property name="removeAbandoned" value="true" />
<!-- 1800秒,也就是30分鐘 -->
<property name="removeAbandonedTimeout" value="1800" />
<!-- 關閉abanded鏈接時輸出錯誤日誌 -->
<property name="logAbandoned" value="true" />
<!-- 打開PSCache,而且指定每一個鏈接上PSCache的大小 若是用Oracle,則把poolPreparedStatements配置爲true,mysql能夠配置爲false。 -->
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize"
value="20" />
<!-- 解密密碼必需要配置的項 -->
<property name="filters" value="wall,stat,config" />
<property name="connectionProperties"
value="config.decrypt=true;config.decrypt.key=${jdbc_publickey}" />
</bean>
在個人資源裏上傳了上面實例的代碼,有須要的支持下,有不當之處,望各位猿友之處,萬分感謝。java
相信您必定對定時任務又有了深入的認識mysql
天天努力一點,天天都在進步。nginx