前言:
传统定时任务存在的问题:
单一应用架构存在单点的风险,另外,如果需要修改定时任务的时间,就需要重新部署整个应用,将会导致整个应用停滞一段时间。
当公司业务越来越多时,All in one的应用系统已经无法满足业务的发展,这时就需要垂直或水平地拆分系统,相应的定时任务调度功能也会被拆分到不同的服务器上运行,此时的问题就是集群中的定时任务会被重复执行。
所以,在分布式场景下定时任务的一个问题就是:怎么让某一个定时任务在一个触发时刻上仅有一台服务区在运行。
在通常情况下可以使用下面几种方法来解决重复执行的问题。
1.只在一台服务器上执行
可以指定所有的调度任务只在固定的单台服务器上执行,虽然该方法解决了重复执行的问题,不过他存在的很明显的两个缺陷:单点风险和资源分布不均衡。
- 单点风险,所有调度任务都在单台服务器上执行,当任务执行节点出现问题,整个定时任务全部终止。
- 资源分配不均衡:随着业务越来越多,相应的定时任务也会增多,单台服务器执行任务的压力会越来越大。
2.通过配置参数分散运行
可以创建一个配置项,其中包含需要执行的定时任务类名,这样可以在部署服务时手动分散定时任务到不同的服务器上。
比如使用config.properties文件配置需要执行的定时任务如下:
timeTaskArr=HelloTask,TestTask
在实现运行的任务中添加条件判断,代码如下:
@Component("helloTask")
public class HelloTask{
@Scheduled(cron="*/10 * * * * ?")
pulbic void doTask(){
if(!timeTaskArr.indexOf(HelloTask.class.getSimpleName())){
//添加不满足时候退出
return;
}
}
}
该方法虽然解决了资源分配不均衡问题,不过依然存在单点风险,同时增加了运维管理的难度。
3.通过全局"锁"互斥执行
可以使用分布式锁来实现,当节点获取到锁就执行任务,在没有获取到锁时就不执行(抢占执行),这样就解决了节点重复执行问题。
可以使用Zookeeper,Redis或者数据库等方式来实现分布式锁。
分布式定时任务极其原理
前面介绍如何解决集群中定时任务重复执行的问题时,看到了很多分散在各个节点上行运行的定时任务,从而引入一个概念:分布式定时任务,那么什么是分布式定时任务呢?
分布式定时任务,指的是将集群中分散到的、可靠性差的定时任务统一化管理和调度,并实现分布式部署的管理方式。
而分布式定时任务框架则是在分布式环境中防止多节点同时执行相同任务时数据被重复处理的框架,处理方式主要分为"抢占式"和"协同式",通过集群的节点分担大批量定时任务的处理,提高批量任务的处理效率。下面介绍这两种方式的区别。
- 抢占式:顾名思义,就是谁先获得资源谁就能执行,这种模式无法将单个任务的数据交给其他节点协同处理,一般用于处理数据量较小、任务较多的场景下。
- 协同式:可以将单个任务处理的数据均分到多个JVM中处理,提高出具的并行处理能力,能够充分利用计算机资源。
分布式定时任务的特点:
- 高可用性:通过分布式部署保证了系统的高可用性,当其中一个节点挂掉时,其他节点仍然可以使用,没有单点风险。
- 可伸缩行:支持弹性伸缩,可以动态增加、删除节点,也可以通过控制台部署和管理定时任务,方便、灵活、高效。
- 负载均衡:通过集群的方式进行管理调度,可以有效的利用资源,达到负载均衡的效果。
- 失效转移:任务都可以持久化到数据库或文件系统中,避免了宕机和数据丢失带来的隐患,同时又完善的任务失败重做机制,和详细的任务跟踪及告警策略。
在分布式定时任务实现原理中最重要的就是分布式锁,而分布式锁是控制分布式系统之间同步访问共享资源的一种方式,在分布式系统中常常需要协调他们的动作。如果在不同的系统或者同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下便需要用到分布式锁。
分布式锁有3中实现方式,具体参考:常用分布式锁实现方式总结
开源分布式定时任务的用法:
Quartz的分布式模式:
集群中的每个节点都是一个独立的Quartz应用,还管理着其他节点。该集群需要分别对每个节点进行启动或停止,不像应用服务器的集群,独一的Quartz节点并不与另一个节点或者管理节点通信,Quartz应用通过数据库来感知另一个应用的存在,也只有使用了JobStore的Quartz才具有集群的功能。接下来基于Quartz实现的分布式定时任务。
首先和Spring整合Quartz集群中不能使用MethodInvokingJobDetailFactoryBean,因为它不可序列化,直接会报错,报错信息
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scheduler' defined in class path resource [applicationContext.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]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:742)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
at com.quartzspring.test.TestQuartz.main(TestQuartz.java:9)
Caused by: 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]
at org.quartz.impl.jdbcjobstore.JobStoreSupport.storeJob(JobStoreSupport.java:1120)
at org.quartz.impl.jdbcjobstore.JobStoreSupport$2.executeVoid(JobStoreSupport.java:1067)
at org.quartz.impl.jdbcjobstore.JobStoreSupport$VoidTransactionCallback.execute(JobStoreSupport.java:3765)
at org.quartz.impl.jdbcjobstore.JobStoreSupport$VoidTransactionCallback.execute(JobStoreSupport.java:3763)
at org.quartz.impl.jdbcjobstore.JobStoreCMT.executeInLock(JobStoreCMT.java:245)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.storeJobAndTrigger(JobStoreSupport.java:1063)
at org.quartz.core.QuartzScheduler.scheduleJob(QuartzScheduler.java:855)
at org.quartz.impl.StdScheduler.scheduleJob(StdScheduler.java:249)
at org.springframework.scheduling.quartz.SchedulerAccessor.addTriggerToScheduler(SchedulerAccessor.java:308)
at org.springframework.scheduling.quartz.SchedulerAccessor.registerJobsAndTriggers(SchedulerAccessor.java:235)
at org.springframework.scheduling.quartz.SchedulerFactoryBean.afterPropertiesSet(SchedulerFactoryBean.java:510)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
... 12 more
Caused by: 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
at org.quartz.impl.jdbcjobstore.StdJDBCDelegate.serializeJobData(StdJDBCDelegate.java:3083)
at org.quartz.impl.jdbcjobstore.StdJDBCDelegate.insertJobDetail(StdJDBCDelegate.java:606)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.storeJob(JobStoreSupport.java:1117)
... 24 more
详细参考:https://objectpartners.com/2013/07/09/configuring-quartz-2-with-spring-in-clustered-mode/
可以使用:JobDetailFactoryBean
前置准备:
下载quartz-2.3.0-distribution包:http://www.quartz-scheduler.org/downloads/files/quartz-2.3.0-distribution.tar.gz
在该路径中quartz-2.3.0-distribution.tar.gz\quartz-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore
tables_mysql_innodb.sql
#
# In your Quartz properties file, you'll need to set
# org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#
#
# By: Ron Cordell - roncordell
# I didn't see this anywhere, so I thought I'd post it here. This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM.
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
CREATE TABLE QRTZ_JOB_DETAILS(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(190) NOT NULL,
JOB_GROUP VARCHAR(190) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_NONCONCURRENT VARCHAR(1) NOT NULL,
IS_UPDATE_DATA VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
JOB_NAME VARCHAR(190) NOT NULL,
JOB_GROUP VARCHAR(190) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(190) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
REPEAT_COUNT BIGINT(7) NOT NULL,
REPEAT_INTERVAL BIGINT(12) NOT NULL,
TIMES_TRIGGERED BIGINT(10) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_CRON_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13,4) NULL,
DEC_PROP_2 NUMERIC(13,4) NULL,
BOOL_PROP_1 VARCHAR(1) NULL,
BOOL_PROP_2 VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_BLOB_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_CALENDARS (
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(190) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (SCHED_NAME,CALENDAR_NAME))
ENGINE=InnoDB;
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_FIRED_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
INSTANCE_NAME VARCHAR(190) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
SCHED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(190) NULL,
JOB_GROUP VARCHAR(190) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID))
ENGINE=InnoDB;
CREATE TABLE QRTZ_SCHEDULER_STATE (
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(190) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (SCHED_NAME,INSTANCE_NAME))
ENGINE=InnoDB;
CREATE TABLE QRTZ_LOCKS (
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME))
ENGINE=InnoDB;
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
commit;
导入到你的mysql数据库中
表名 | 说明 |
---|---|
QRTZ_BLOG_TRIGGERS | Trigger 作为 Blob 类型存储(用于 Quartz 用户用JDBC创建他们自己定制的 Trigger 类型,JobStore并不知道如何存储实例的时候) |
QRTZ_CALENDARS | 存储Quartz的Calendar信息,Quartz可配置一个日历来指定一个时间范围 |
QRTZ_CRON_TRIGGERS | 存储CronTrigger,包括Cron表达式和时区信息 |
QRTZ_FIRED_TRIGGERS | 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息 |
QRTZ_JOB_DETAILS | 存储的Job的详细信息,包含[IS_DURABLE]是否持久化、[IS_NONCONCURRENT]是否非并发等信息 |
QRTZ_LOCKS | 存储程序的悲观锁的信息 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的Trigger组的信息 |
QRTZ_SCHEDULER_STATE | 存储少量的有关 Scheduler 的状态信息,和别的Scheduler实例(假如是用于一个集群中) |
QRTZ_SIMPLE_TRIGGERS | 存储简单的Trigger,包括重复次数、间隔、以及已触的次数 |
QRTZ_TRIGGERS | 存储Trigger的信息,包括[START_TIME]开始执行时间,[END_TIME]结束执行时间,[PREV_FIRE_TIME]上次执行时间,[NEXT_FIRE_TIME]下次执行时间,[TRIGGER_TYPE]触发器类型:simple和cron,[TRIGGER_STATE]执行状态:WAITING,PAUSED,ACQUIRED分别为:等待,暂停,运行中 |
添加依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.43</version>
</dependency>
数据源的选择:
默认使用的c3p0数据源,quartz的依赖默认依赖了c3p0的数据源
直接在quartz.properties文件中添加就可以使用了
#org.quartz.jobStore.dataSource = qzDS
#org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
#org.quartz.dataSource.qzDS.URL = jdbc:mysql://localhost:3306/quartzdb?useUnicode=true&characterEncoding=UTF-8
#org.quartz.dataSource.qzDS.user = root
#org.quartz.dataSource.qzDS.password = tiger
#org.quartz.dataSource.qzDS.maxConnections = 10
#org.quartz.dataSource.qzDS.acquireIncrement=1
druid,c3p0任意选择一个数据源依赖添加到项目中就行了
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!--如果在xml中配置数据源,xml配置文件中配置的数据源会覆盖你在quartz.properties文件中配置的数据源,以下为两种数据源的Demo-->
<!--druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.1</version>
</dependency>
<!--c3p0数据源-->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
在applicationContext-quartz.xml文件中配置数据源,以下为c3p0,druid的Demo,使用的时候,如果不在quartz.properties文件中配置数据源连接信息的话,就使用以下方式配置。
<!-- 配置数据库连接,阿里数据源druid连接池 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url"
value="jdbc:mysql://localhost:3306/quartzdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="tiger"/>
<property name="filters" value="stat"/>
<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="minIdle" value="1"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="poolPreparedStatements" value="true"/>
<property name="maxOpenPreparedStatements" value="20"/>
</bean>
<!-- 配置C3p0数据源 -->
<bean id="c3p0DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/quartzdb" />
<property name="user" value="root" />
<property name="password" value="tiger" />
<!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
<property name="initialPoolSize" value="3" />
<!--连接池中保留的最小连接数。Default: 3 -->
<property name="minPoolSize" value="3" />
<!--连接池中保留的最大连接数。Default: 15 -->
<property name="maxPoolSize" value="15" />
<!--最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
<property name="maxIdleTime" value="60" />
<!--每60秒检查所有连接池中的空闲连接。Default: 0 -->
<property name="idleConnectionTestPeriod" value="60" />
<property name="acquireIncrement" value="2"/>
</bean>
在SchedulerFactoryBean中配置你想要使用的数据源就行了,这里使用的是Durid数据源
<property name="dataSource" ref="druidDataSource"></property>
案例工程结构为如下:
在这里使用Durid数据源applicationContext-quartz.xml配置
<!-- 配置数据库连接,阿里数据源druid连接池 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url"
value="jdbc:mysql://localhost:3306/quartzdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="tiger"/>
<property name="filters" value="stat"/>
<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="minIdle" value="1"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="poolPreparedStatements" value="true"/>
<property name="maxOpenPreparedStatements" value="20"/>
</bean>
<bean id="userServiceImpl" class="com.quartzspring.service.impl.UserServiceImpl"/>
<bean id="helloJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<!-- 用到的job实例 -->
<property name="jobClass" value="com.quartzspring.job.HelloJob"/>
<property name="name" value="helloJobName"></property>
<property name="group" value="helloJobGroup"></property>
<!-- 没有绑定触发器仍然保留在Quartz的JobStore中 ,默认为false-->
<property name="Durability" value="true"/>
<property name="jobDataAsMap">
<map>
<description>jobDataAsMap</description>
<!-- key 属性值,value 对应的bean -->
<entry key="name" value="张三"/>
<entry key="userService" value-ref="userServiceImpl"/>
</map>
</property>
</bean>
<bean id="worldJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<!-- 用到的job实例 -->
<property name="jobClass" value="com.quartzspring.job.WorldJob"/>
<property name="name" value="worldJobName"></property>
<property name="group" value="worldJobGroup"></property>
<!-- 没有绑定触发器仍然保留在Quartz的JobStore中 ,默认为false-->
<property name="Durability" value="true"/>
</bean>
<!-- 配置触发器Trigger -->
<bean id="hellloTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="helloJobDetail"/>
<property name="name" value="helloTriggerName"></property>
<property name="group" value="helloTriggerGroup"></property>
<property name="cronExpression">
<!-- 配置cron表达式,每2秒执行一次 -->
<value>0/1 * * * * ?</value>
</property>
</bean>
<bean id="worldTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="worldJobDetail"/>
<property name="name" value="worldTriggerName"></property>
<property name="group" value="worldTriggerGroup"></property>
<property name="cronExpression">
<!-- 配置cron表达式,每1秒执行一次 -->
<value>0/1 * * * * ?</value>
</property>
</bean>
<!-- 配置scheduler工厂 -->
<!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序 -->
<!-- 如果lazy-init='true',则需要实例化该bean才能执行调度程序 -->
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" lazy-init="false" >
<!--scheduler调度器的名称-->
<property name="schedulerName" value="myScheduler"/>
<!--通过SchedulerFactoryBean的configLocation属性指定Quartz配置文件的位置。 -->
<property name="configLocation" value="classpath:quartz.properties"></property>
<!--scheduler被初始化之后,自动启动-->
<property name="autoStartup" value="true"/>
<!-- 指定延迟30秒开始,避免系统未完全启动却开始执行定时任务的情况 -->
<!--<property name="startupDelay" value="30" />-->
<!--配置数据源-->
<property name="dataSource" ref="druidDataSource"></property>
<!--可选,覆盖已存在的任务,QuartzScheduler启动会更新已存在的Job,会更新表达式子等-->
<property name="overwriteExistingJobs" value="true" />
<!-- 注册触发器 -->
<property name="triggers">
<list>
<ref bean="hellloTrigger"/>
<ref bean="worldTrigger"/>
</list>
</property>
</bean>
在总applicationContext.xml导入就行了。
HelloJob.java
package com.quartzspring.job;
import com.quartzspring.service.UserService;
import org.quartz.*;
import org.springframework.scheduling.quartz.QuartzJobBean;
//@DisallowConcurrentExecution //保证上一次任务执行完毕再执行下一任务
//@PersistJobDataAfterExecution //上一个任务完成前写入需要被下一个任务获取的变量以及对应的属性值,类似求和累加
public class HelloJob extends QuartzJobBean {
private UserService userService;
private String name;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("定时任务HelloJob开始执行,name为:"+name);
userService.getUserInof();
}
public void setUserService(UserService userService) {
this.userService = userService;
}
public void setName(String name) {
this.name = name;
}
}
WorldJob.java
package com.quartzspring.job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
public class WorldJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("WorldJob执行!!!");
}
}
UserService.java
package com.quartzspring.service;
import java.io.Serializable;
public interface UserService extends Serializable {
void getUserInof();
}
UserServiceImpl.java
package com.quartzspring.service.impl;
import com.quartzspring.service.UserService;
public class UserServiceImpl implements UserService {
@Override
public void getUserInof() {
System.err.println("UserServiceImpl被调用");
}
}
TestQuartz.java
package com.quartzspring.test;
import org.quartz.SchedulerException;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestQuartz {
public static void main(String[] args) throws SchedulerException {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}
quartz.properties
#调度标识名 集群中每一个实例都必须使用相同的名称
org.quartz.scheduler.instanceName = quartzScheduler
#调度器实例编号自动生成,每个实例不能不能相同
org.quartz.scheduler.instanceId = AUTO
#开启分布式部署,集群
org.quartz.jobStore.isClustered = true
#分布式节点有效性检查时间间隔,单位:毫秒,默认值是15000
org.quartz.jobStore.clusterCheckinInterval = 2000
#远程管理相关的配置,全部关闭
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
#实例化ThreadPool时,使用的线程类为SimpleThreadPool(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
##并发个数,指定线程数,至少为1(无默认值)(一般设置为1-100之间的的整数合适)
org.quartz.threadPool.threadCount = 10
##设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority = 5
#org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
#容许的最大作业延长时间,最大能忍受的触发超时时间,如果超过则认为“失误”,不敢再内存中还是数据中都要配置
org.quartz.jobStore.misfireThreshold = 6000
#持久化方式配置
# 默认存储在内存中,保存job和Trigger的状态信息到内存中的类
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#数据库方式
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#持久化方式配置数据驱动,MySQL数据库
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#quartz相关数据表前缀名
org.quartz.jobStore.tablePrefix = QRTZ_
#数据库别名 随便取
#org.quartz.jobStore.dataSource = qzDS
#org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
#org.quartz.dataSource.qzDS.URL = jdbc:mysql://192.168.184.135:3306/quartzdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8
#org.quartz.dataSource.qzDS.user = root
#org.quartz.dataSource.qzDS.password = 123456
#org.quartz.dataSource.qzDS.maxConnections = 10
#org.quartz.dataSource.qzDS.acquireIncrement=1
log4j.properties
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=C:/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=info,stdout
启动两次main方法,在控制台上可以看到
可以看到到启动两次,这两个Job在这两个项目中负载执行调度。
节点争抢Job问题
因为Quartz使用了一个随机的负载均衡算法, Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。
参考:
《可伸缩服务架构 框架与中间件》
如果在实践过程中出现问题可参考: