quartz

前言
几种任务调度比较
quart的API分析
常用配置
cron表达式
总结
前言
我们会经常用到定时任务,比如每天凌晨生成前天报表,每一小时生成汇总数据,发送通知等等。换句话说就是在某一个有规律的时间点干某件事。并且时间的触发的条件可以非常复杂(比如每月最后一个工作日的17:50),复杂到需要一个专门的框架来干这个事。

Quartz就是来干这样的事,你给它一个触发条件的定义,它负责到了时间点,触发相应的Job起来干活。

   这种情况下使用Quartz框架无疑是非常好的选择,并且与Spring可以非常方便的集成。

几种任务调度
Timer,简单无门槛,一般也没人用。
spring @Scheduled注解,一般集成于项目中,小任务很方便。
开源工具 Quartz,分布式集群开源工具,以下两个分布式任务都是基于Quartz实现。
分布式任务 XXL-JOB,是一个轻量级分布式任务调度框架,支持通过 Web 页面对任务进行 CRUD 操作,支持动态修改任务状态、暂停/恢复任务,以及终止运行中任务,支持在线配置调度任务入参和在线查看调度结果。
分布式任务 Elastic-Job,是一个分布式调度解决方案,由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成。定位为轻量级无中心化解决方案,使用 jar 包的形式提供分布式任务的协调服务。支持分布式调度协调、弹性扩容缩容、失效转移、错过执行作业重触发、并行调度、自诊。
分布式任务 Saturn,Saturn是唯品会在github开源的一款分布式任务调度产品。它是基于当当elastic-job来开发的,其上完善了一些功能和添加了一些新的feature。其在唯品会内部已经发部署350+个节点,每天任务调度4000多万次。同时,管理和统计也是它的亮点。
quart的介绍

Scheduler:任务调度器,所有的任务都是从这里开始。
Trigger:触发器,定义任务执行的方式、间隔。
JobDetail & Job : 定义任务具体执行的逻辑。
Scheduler

      scheduler 是quartz的核心所在,所有的任务都是通过scheduler开始。Scheduler的生命期,从SchedulerFactory创建它时开始,到Scheduler调用shutdown()方法时结束;Scheduler被创建后,可以增加、删除和列举Job和Trigger,以及执行其它与调度相关的操作(如暂停Trigger)。但是,Scheduler只有在调用start()方法后,才会真正地触发trigger(即执行job)。

在这里插入图片描述

      scheduler是一个接口类,所有的具体实现类都是通过SchedulerFactory工厂类实现,但是SchedulerFactory有两个具体的实现类:
      StdSchedulerFactory:默认值加载是当前工作目录下的”quartz.properties”属性文件。如果加载失败,会去加载org/quartz包下的”quartz.properties”属性文件。一般使用这个实现类就能满足我们的要求。
      DirectSchedulerFactory:是为那些想绝对控制 Scheduler 实例是如何生产出的人所设计的。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
Trigger

withIdentity() 给触发器一些属性 比如名字,组名。

startNow() 立刻启动

withSchedule(ScheduleBuilder schedBuilder) 以某种触发器触发。

usingJobData(String dataKey, Boolean value) 给具体job传递参数。

举个创建Trigger的例子:

Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(“trigger1”, “group1”)
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()).withIntervalInSeconds(1)

.repeatForever()).build();

Calendar用于从trigger的调度计划中排除时间段。比如,可以创建一个trigger,每个工作日的上午9:30执行,然后增加一个Calendar,排除掉所有的商业节日。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Trigger的重点内容就是在withSchedule这个方法,从参数开始:查看SchedulerBuilder,这个是个抽象类,一共有4种具体实现,如图:

在这里插入图片描述
在这里插入图片描述
SimpleScheduleBuilder
最简单的触发器,表示从某一时刻开始,以一定的时间间隔执行任务。
属性:
repeatInterval 重复间隔。
repeatCount 重复次数。
比如:现在开始,以后每一个小时执行一次。

Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(“trigger1”, “group1”)
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()

              .withIntervalInHours(1)
             .repeatForever()).build();

DailyTimeIntervalScheduleBuilder
每一天的某一个时间段内,以一定的时间间隔执行任务,可以指定具体的某一天(星期一、星期二、星期三。。)
属性:
intervalUnit 重复间隔(秒、分钟、小时。。。)。
daysOfWeek 具体的星期。 默认 周一到周日
startTimeOfDay 每天开始时间 默认 0.0
endTimeOfDay 每天结束时间,默认 23.59.59
repeatCount 重复次数。 默认是-1 不限次数
interval 每次执行间隔
比如每周一到周四早上9点开始,晚上16点结束,每次执行间隔1 小时。

需要 导入静态方法:import static org.quartz.DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule;

Trigger trigger = TriggerBuilder.newTrigger().withIdentity(“trigger1”, “group1”)
//加入 scheduler之后立刻执行
.startNow()
//定时 ,每个1秒钟执行一次
.withSchedule(dailyTimeIntervalSchedule()
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) //第天9:00开始
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(16, 0)) //16:00 结束
.onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY) //周一至周五执行
.withIntervalInHours(1) //每间隔1小时执行一次
).build();

CalendarIntervalScheduleBuilder
和SimpleScheduleBuilder类似,都是表示从某一时刻开始,以一定时间间隔执行任务。但是SimpleScheduleBuilder无法指定一些特殊情况,比如每个月执行一次,每周执行一次、每一年执行一次
属性:
interval 执行间隔
intervalUnit 执行间隔的单位(秒,分钟,小时,天,月,年,星期)

Trigger trigger = TriggerBuilder.newTrigger().withIdentity(“trigger1”, “group1”)
//加入 scheduler之后立刻执行
.startNow()
//定时 ,每个1秒钟执行一次
.withSchedule(calendarIntervalSchedule()
.withIntervalInWeeks(1) //每周执行一次
).build();

CronScheduleBuilder

CronScheduleBuilder
以上几个例子都可以使用cron表达式来表示。
cron表达式。
Trigger trigger = TriggerBuilder.newTrigger().withIdentity(“trigger1”, “group1”)
//加入 scheduler之后立刻执行
.startNow()
//定时 ,每个1秒钟执行一次
.withSchedule(cronSchedule(“0 0/2 8-17 * * ?”) // 每天8:00-17:00,每隔2分钟执行一次
).build();

JobDetail & Job

jobdetail 就是对job的定义,而job是具体执行的逻辑内容。 具体的执行的逻辑需要实现 job类,并实现execute方法。
这里为什么需要有个JobDetai来作为job的定义,为什么不直接使用job?
如果使用jobdetail来定义,那么每次调度都会创建一个new job实例,这样带来的好处就是任务并发执行的时候,互不干扰,不会对临界资源造成影响。

我们传给scheduler一个JobDetail实例,因为我们在创建JobDetail时,将要执行的job的类名传给了JobDetail,所以scheduler就知道了要执行何种类型的job;每次当scheduler执行job时,在调用其execute(…)方法之前会创建该类的一个新的实例;执行完毕,对该实例的引用就被丢弃了,实例会被垃圾回收;这种执行策略带来的一个后果是,job必须有一个无参的构造函数(当使用默认的JobFactory时);另一个后果是,在job类中,不应该定义有状态的数据属性,因为在job的多次执行中,这些属性的值不会保留。

那么如何给job实例增加属性或配置呢?如何在job的多次执行中,跟踪job的状态呢? JobDataMap

当一个trigger被触发时,与之关联的JobDetail实例会被加载,JobDetail引用的job类通过配置在Scheduler上的JobFactory进行初始化。

简单的实现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Job的实例要到该执行它们的时候才会实例化出来。每次 Job 被执行,一个新的 Job 实例会被创建。
这样你的Job 不必担心线程安全性,因为同一时刻仅有一个线程去执行给定 Job 类的实例,甚至是并发执行同一 Job 也是如此。
@DisallowConcurrentExecution 保证上一个任务执行完后,再去执行下一个任务,这里的任务是同一个任务。

cron 表达式
表达式站位说明
如:0 0 12 * * ?

corn表达式说明图

星号():可用在所有字段中,表示对应时间域的每一个时刻,例如, 在分钟字段时,表示“每分钟”;
问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;
减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;
逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;
斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y;
L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五;
W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围;
LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日;
井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;
C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。
Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。
常用例子写法:

配置文件配置

在集群中每个实例都必须有一个唯一的instanceId,但是应该有一个相同的instanceName【默认“QuartzScheduler”】【非必须】

org.quartz.scheduler.instanceName = MyClusteredScheduler

Scheduler实例ID,全局唯一,【默认值NON_CLUSTERED】,或者可以使用“SYS_PROP”通过系统属性设置id。【非必须】

org.quartz.scheduler.instanceId = AUTO

只有在”org.quartz.scheduler.instanceId”设置为”AUTO”的时候才使用该属性设置。

默认情况下,“org.quartz.simpl.SimpleInstanceIdGenerator”是基于instanceId和时间戳来自动生成的。

其他的id生成器的实现包括 SystemPropertyInstanceIdGenerator 从系统属性获取 “org.quartz.scheduler.instanceId”, 和 HostnameInstanceIdGenerator 使用主机名 (InetAddress.getLocalHost().getHostName())。也可以自定义生成方式【默认org.quartz.simpl.SimpleInstanceIdGenerator】【非必须】

#org.quartz.scheduler.instanceIdGenerator.class = org.quartz.simpl.SimpleInstanceIdGenerator

指定线程名,如果不指定的话,会自动使用org.quartz.scheduler.instanceName属性值加上后缀字符串”_QuartzSchedulerThread”.【默认instanceName+’_QuartzSchedulerThread’】【非必须】

#org.quartz.scheduler.threadName =

指定scheduler的主线程是否为后台线程,【默认false】【非必须】

#org.quartz.scheduler.makeSchedulerThreadDaemon = false

指定Quartz生成的线程是否继承初始化线程的上下文类加载器。这会影响Quartz的主调度线程、JDBCJobStore的”熄火”处理线程、集群回复线程和线程池里的线程。 将该值设置为“true”可以帮助类加载,JNDI查找,并在应用程序服务器上使用Quartz等相关问题,【默认false】【非必须】

#org.quartz.scheduler.threadsInheritContextClassLoaderOfInitializer = false

在调度程序空闲的时候,重复查询是否有可用触发器的等待时间。通常并不会设置为true,除非你是用XA事务,并且延迟触发会导致问题的场景。 5000ms以下是不推荐的,因为它会导致过的的数据库查询。1000ms以下是非法的。【默认30000】【非必须】

#org.quartz.scheduler.idleWaitTime = 30000

连接超时重试连接的间隔。使用 RamJobStore时,该参数并没什么用【默认15000】【非必须】

#org.quartz.scheduler.dbFailureRetryInterval = 15000

最可靠的方式就是使用【默认”org.quartz.simpl.CascadingClassLoadHelper”】,没必要指定其他类【非必须】

#org.quartz.scheduler.classLoadHelper.class = org.quartz.simpl.CascadingClassLoadHelper

指定JobFactory的类(接口)名称。负责实例化jobClass。【默认”org.quartz.simpl.PropertySettingJobFactory”】,只是在job被执行的时候简单调用newInstance()实例化一个job类。PropertySettingJobFactory 会使用反射机制通过SchedulerContext、 Job、Trigger和 JobDataMaps设置job bean的属性。在使用JTA事务时,可设置事务相关的属性【非必须】

#org.quartz.scheduler.jobFactory.class = org.quartz.simpl.PropertySettingJobFactory

#org.quartz.context.key.SOME_KEY = none

设置Quartz能够加载UserTransaction换利器的JNDI的 URL。Websphere 的用户可能会设置为“jta/usertransaction。只有在Quartz使用JobStoreCMT的时候,才会使用该属性,并且org.quartz.scheduler.wrapJobExecutionInUserTransaction也会设置为true。默认【默认值是”java:comp/UserTransaction”】【非必须】

#org.quartz.scheduler.userTransactionURL = java:comp/UserTransaction

如果想使用Quartz在执行一个job前使用UserTransaction,则应该设置该属性为true。job执行完、在JobDataMap改变之后事务会提交。默认值是false。 可以在你的job类中使用 @ExecuteInJTATransaction注解, 可以控制job是否使用事务。【默认false】【非必须】

#org.quartz.scheduler.wrapJobExecutionInUserTransaction = false

建议设置为“org.terracotta.quartz.skipUpdateCheck=true”不会在程序运行中还去检查quartz是否有版本更新。【默认false】【非必须】

#org.quartz.scheduler.skipUpdateCheck = true

许调度程序一次性触发的触发器数量。.默认值是1。值越大一次性触发的任务就可以越多,但是在集群环境【非必须】下,不建议设置为很大值。如果值 > 1, 并且使用了 JDBC JobStore的话, org.quartz.jobStore.acquireTriggersWithinLock属性必须设置为true,以避免”弄脏”数据。【默认1】

#org.quartz.scheduler.batchTriggerAcquisitionMaxCount = 1

允许触发器被获取并在其预定的触发时间之前触发的数量。【默认0】【非必须】

#org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow = 0

#============================================================================

Configure ThreadPool 线程池属性

#============================================================================

线程池的实现类(定长线程池,几乎可满足所有用户的需求)【默认null】【必须】

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)【默认-1】【必须】

org.quartz.threadPool.threadCount = 25

设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1)【默认Thread.NORM_PRIORITY (5)】【非必须】

org.quartz.threadPool.threadPriority = 5

设置SimpleThreadPool的一些属性,一般使用默认值

设置是否为守护线程

#org.quartz.threadpool.makethreadsdaemons = false

#org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

#org.quartz.threadpool.threadsinheritgroupofinitializingthread = false

#线程前缀默认值是:[Scheduler Name]_Worker

#org.quartz.threadpool.threadnameprefix = swhJobThead;

配置全局监听(TriggerListener,JobListener) 则应用程序可以接收和执行 预定的事件通知

Configuring a Global TriggerListener 配置全局的Trigger监听器。

MyTriggerListenerClass 类必须有一个无参数的构造函数,和 属性的set方法,目前2.2.x只支持原始数据类型的值(包括字符串)

#org.quartz.triggerListener.NAME.class = com.swh.MyTriggerListenerClass

#org.quartz.triggerListener.NAME.propName = propValue

#org.quartz.triggerListener.NAME.prop2Name = prop2Value

Configuring a Global JobListener 配置全局的Job监听器

MyJobListenerClass 类必须有一个无参数的构造函数,和 属性的set方法,目前2.2.x只支持原始数据类型的值(包括字符串)

#org.quartz.jobListener.NAME.class = com.swh.MyJobListenerClass

#org.quartz.jobListener.NAME.propName = propValue

#org.quartz.jobListener.NAME.prop2Name = prop2Value

#============================================================================

Configure JobStore 配置数据存储的方式

#============================================================================

使用RAMJobStore配置

将schedule相关信息保存在RAM中,轻量级,速度快,遗憾的是应用重启时相关信息都将丢失。

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

最大能忍受的触发超时时间(触发器被认定为“misfired”之前),如果超过则认为“失误”【默认60秒】

org.quartz.jobStore.misfireThreshold = 60000

使用JDBCJobStore配置

所有的quartz数据例如job和Trigger的细节信息被保存在内存或数据库中,有两种实现:JobStoreTX(自己管理事务)和JobStoreCMT(application server管理事务,即全局事务JTA)

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

最大能忍受的触发超时时间(触发器被认定为“misfired”之前),如果超过则认为“失误”【默认60秒】

org.quartz.jobStore.misfireThreshold = 60000

类似于Hibernate的dialect,用于处理DB之间的差异,StdJDBCDelegate能满足大部分的DB

org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

配置数据源的名称,在后面配置数据源的时候要用到,例如org.quartz.dataSource.clusterDS.driver = com.mysql.jdbc.Driver

org.quartz.jobStore.dataSource = clusterDS

数据表前缀

org.quartz.jobStore.tablePrefix = QRTZ_

为了指示JDBCJobStore所有的JobDataMaps中的值都是字符串,并且能以“名字-值”对的方式存储而不是以复杂对象的序列化形式存储在BLOB字段中,应该设置为true(缺省方式)

org.quartz.jobStore.useProperties = true

是否集群、负载均衡、容错,如果应用在集群中设置为false会出错

org.quartz.jobStore.isClustered = true

检入到数据库中的频率(毫秒)。检查是否其他的实例到了应当检入的时候未检入这能指出一个失败的实例,且当前Scheduler会以此来接管执行失败并可恢复的Job通过检入操作,Scheduler也会更新自身的状态记录

org.quartz.jobStore.clusterCheckinInterval = 20000

jobStore处理未按时触发的Job的数量

#org.quartz.jobStore.maxMisfiresToHandleAtATime = 20

true/false,true则调用connection的setAutoCommit(false)方法

org.quartz.jobStore.dontSetAutoCommitFalse = true

加锁的SQL语句,默认为SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE

{0}=$@org.quartz.jobStore.tablePrefix

org.quartz.jobStore.selectWithLockSQL = false

true/false, true则调用connection的setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) 方法

org.quartz.jobStore.txIsolationLevelSerializable = false

触发job时是否需要拥有锁

org.quartz.jobStore.acquireTriggersWithinLock = true

用于管理数据库中相关信息的锁机制的类名

org.quartz.jobStore.lockHandler.class =

#============================================================================

Configure Datasources 数据源

#============================================================================

使用常规数据源配置

数据库驱动

org.quartz.dataSource.NAME.driver = com.mysql.jdbc.Driver

数据库连接地址

org.quartz.dataSource.NAME.URL = jdbc:mysql://${mysql.address}/etc-quartz?useUnicode=true&characterEncoding=utf8

数据库用户名

org.quartz.dataSource.NAME.user = ${mysql.user}

数据库密码

org.quartz.dataSource.NAME.password = ${mysql.password}

数据库最大连接数(如果Scheduler很忙,比如执行的任务与线程池的数量差不多相同,那就需要配置DataSource的连接数量为线程池数量+1)

org.quartz.dataSource.NAME.maxConnections = 30

dataSource用于检测connection是否failed/corrupt的SQL语句

org.quartz.dataSource.NAME.validationQuery=select RAND()

使用JNDI数据源配置

JNDI URL

org.quartz.dataSource.NAME.jndiURL =

JNDI InitialContextFactory

org.quartz.dataSource.NAME.java.naming.factory.initial =

后三行为连接到JNDI提供者的相关信息

org.quartz.dataSource.NAME.java.naming.provider.url =

org.quartz.dataSource.NAME.java.naming.security.principal =

org.quartz.dataSource.NAME.java.naming.security.credentials =

用户自定义org.quartz.utils.ConnectionProvider实现类

org.quartz.dataSource.NAME.connectionProvider.class =

org.quartz.dataSource.NAME.XXX =

除了配置文件配置还可以使用代码配置:

在这里插入图片描述

这里未讲的稍微高级的主题
JobStore 介绍、配置
集群: 介绍、配置
RMI
监听器 TriggerListeners and JobListeners、SchedulerListeners
插件
总结一下:
创建一个具体的任务(Job)
配置任务的触发时间等(Trigger)
配置任务的具体内容(JobDetail)
调度器Scheduled根据JobDetail+Trigger安排此任务去执行
在这里插入图片描述

任务的执行时序图:
在这里插入图片描述

上半部分展现的是任务执行之前准备工作的时序,下半部分展现的是任务执行的时序。

步骤1.调度线程首先去线程池中获取可用的线程,如果没有的话,就阻塞。

步骤2.从JobStore(从存储介质中获取触发器,存储介质可以是内存也可以是数据库)获取(接下来30s内的)触发器,然后等待该触发器触发。

步骤3.调度线程创建一个JobRunShell(就是一个Runnable),然后从线程池中调用线程执行该任务。

接下来就是任务执行的时序:

步骤4.获取trigger、JobDetail以及生成Job实例,然后执行job的execute接口函数。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值