Quartz:翻译是石英,手上石英表表示天生是个定时框架,既然网上资料一大堆,不如查查官网记录。
资料地址:https://www.w3cschool.cn/quartz\_doc/quartz\_doc-1xbu2clr.html
定时任务实现的几种方式:
Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。
ScheduledExecutorService:也是jdk自带的一个类;是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。
Spring Task:Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。
Quartz:这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂。
一、Quartz API,Jobs和Triggers
什么是Quartz?
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能:持久性作业 - 就是保持调度定时的状态;作业管理 - 对调度作业进行有效的管理;
定时任务的平时需求还是比较多,查询资料转载后整理。例如:定时打印token,下单后定时查看订单状态,外卖平台,优惠券等
在我们实际的项目中,当Job过多的时候就需要一个任务调度框架,帮我们自动去执行这些程序。那么该如何实现这个功能呢?
(1)首先我们需要定义实现一个定时功能的接口,我们可以称之为Task(或Job),如定时发送邮件的task(Job),重启机器的task(Job),优惠券到期发送短信提醒的task(Job),实现接口如下:
(2)有了任务之后,还需要一个能够实现触发任务去执行的触发器,触发器Trigger最基本的功能是指定Job的执行时间,执行间隔,运行次数等。
(3)有了Job和Trigger后,怎么样将两者结合起来呢?即怎样指定Trigger去执行指定的Job呢?这时需要一个Schedule,来负责这个功能的实现。
调度器:Scheduler
任务:JobDetail
触发器:Trigger,包括SimpleTrigger和CronTrigger
二、Quartz快速入门
下面来利用Quartz搭建一个最基本的Demo。
1、导入依赖的jar包:
org.quartz-scheduler quartz 2.3.0
2、新建一个能够打印任意内容的类:
public class PrintWordsJob implements Job{ @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { String printTime = new SimpleDateFormat("yy-MM-dd HH-mm-ss").format(new Date()); System.out.println("PrintWordsJob start at:*************" + printTime + ", prints: Hello Job---------------" + new Random().nextInt(100)); } }
3、实例化并启动一个scheduler,调度执行一个job
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory(); Scheduler sched = schedFact.getScheduler(); sched.start(); // define the job and tie it to our HelloJob class JobDetail job = newJob(HelloJob.class) .withIdentity("myJob", "group1") .build(); // Trigger the job to run now, and then every 40 seconds Trigger trigger = newTrigger() .withIdentity("myTrigger", "group1") .startNow() .withSchedule(simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build(); // Tell quartz to schedule the job using our trigger sched.scheduleJob(job, trigger);
三、使用Quartz
下面就程序中出现的几个参数,看一下Quartz框架中的几个重要参数:
Job和JobDetail
JobExecutionContext
JobDataMap
Trigger、SimpleTrigger、CronTrigger
(1)Job和JobDetail
Job是Quartz中的一个接口,接口下只有execute方法,在这个方法中编写业务逻辑。
接口中的源码:
package org.quartz;public interface Job { void execute(JobExecutionContext var1) throws JobExecutionException;}
JobDetail用来绑定Job,为Job实例提供许多属性:
name
group
jobClass
jobDataMap
JobDetail绑定指定的Job,每次Scheduler调度执行一个Job的时候,首先会拿到对应的Job,然后创建该Job实例,再去执行Job中的execute()的内容,任务执行结束后,关联的Job对象实例会被释放,且会被JVM GC清除。
为什么设计成JobDetail + Job,不直接使用Job
我们传给scheduler一个JobDetail实例,因为我们在创建JobDetail时,将要执行的job的类名传给了JobDetail,所以scheduler就知道了要执行何种类型的job;每次当scheduler执行job时,在调用其execute(…)方法之前会创建该类的一个新的实例;执行完毕,对该实例的引用就被丢弃了,实例会被垃圾回收;这种执行策略带来的一个后果是,job必须有一个无参的构造函数(当使用默认的JobFactory时);另一个后果是,在job类中,不应该定义有状态的数据属性,因为在job的多次执行中,这些属性的值不会保留
(2)JobExecutionContext
JobExecutionContext中包含了Quartz运行时的环境以及Job本身的详细数据信息。
当Schedule调度执行一个Job的时候,就会将JobExecutionContext传递给该Job的execute()中,Job就可以通过JobExecutionContext对象获取信息。
主要信息有:
public interface JobExecutionContext { Scheduler getScheduler(); Trigger getTrigger(); Calendar getCalendar(); boolean isRecovering(); TriggerKey getRecoveringTriggerKey() throws IllegalStateException; int getRefireCount(); JobDataMap getMergedJobDataMap(); JobDetail getJobDetail(); Job getJobInstance(); Date getFireTime(); Date getScheduledFireTime(); Date getPreviousFireTime(); Date getNextFireTime(); String getFireInstanceId(); Object getResult(); void setResult(Object var1); long getJobRunTime(); void put(Object var1, Object var2); Object get(Object var1);}
(3)JobExecutionContext
Jobs很容易实现,在接口中只有一个“execute”方法。本节主要关注:Job的特点、Job接口的execute方法以及JobDetail。
你定义了一个实现Job接口的类,这个类仅仅表明该job需要完成什么类型的任务,除此之外,Quartz还需要知道该Job实例所包含的属性;这将由JobDetail类来完成。
JobDetail实例是通过JobBuilder类创建的,导入该类下的所有静态方法,会让你编码时有DSL的感觉:
如:
obDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .usingJobData("jobDetail1", "这个Job用来测试的") .withIdentity("job1", "group1").build(); Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1") .usingJobData("trigger1", "这是jobDetail1的trigger") .startNow()//立即生效 .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(1)//每隔1s执行一次 .repeatForever()).build();//一直执行
Job执行的时候,可以获取到这些参数信息:
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println(jobExecutionContext.getJobDetail().getJobDataMap().get("jobDetail1")); System.out.println(jobExecutionContext.getTrigger().getJobDataMap().get("trigger1")); String printTime = new SimpleDateFormat("yy-MM-dd HH-mm-ss").format(new Date()); System.out.println("start:" + printTime + " 第-" + new Random().nextInt(100)); }
(4)Trigger
Trigger
与job一样,trigger也很容易使用,但是还有一些扩展选项需要理解,以便更好地使用quartz。trigger也有很多类型,我们可以根据实际需要来选择。。
new Trigger().startAt():表示触发器首次被触发的时间;new Trigger().endAt():表示触发器结束触发的时间;
SimpleTrigger
SimpleTrigger可以满足的调度需求是:在具体的时间点执行一次,或者在具体的时间点执行,并且以指定的间隔重复执行若干次。比如,你有一个trigger,你可以设置它在2015年1月13日的上午11:23:54准时触发,或者在这个时间点触发,并且每隔2秒触发一次,一共重复5次。
下面的程序就实现了程序运行5s后开始执行Job,执行Job 5s后结束执行:
/** * @author zhaokkstart * @create 2019-07-02 16:13 */public class MyScheduler { public static void main(String[] args) throws SchedulerException { StdSchedulerFactory stdSchedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = stdSchedulerFactory.getScheduler(); JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .withIdentity("job1", "group1") .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "triggerGroup1") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(1)//每隔1s执行一次 .repeatForever()).build(); scheduler.scheduleJob(jobDetail,trigger); System.out.println("--------start ! ------------"); scheduler.start(); try{ TimeUnit.SECONDS.sleep(10);}catch(InterruptedException e) {e.printStackTrace();} scheduler.shutdown(); System.out.println("--------end ! ------------"); }}
CronTrigger
CronTrigger通常比Simple Trigger更有用,如果您需要基于日历的概念而不是按照SimpleTrigger的精确指定间隔进行重新启动的作业启动计划。CroTrigger是基于Cron表达式的,先了解下Cron表达式:
其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于”月份中的日期”和”星期中的日期”这两个元素互斥的,必须要对其中一个设置
如:* 30 10 ? * 1/5 *
表示(从后往前看)
[指定年份] 的[ 周一到周五][指定月][不指定日][上午10时][30分][指定秒]
又如:00 00 00 ?* 10,11,12 1#5 2018
表示2018年10、11、12月的第一周的星期五这一天的0时0分0秒去执行任务。
可通过在线生成Cron表达式的工具:
Quartz高级(企业)功能
由 Alma 创建, 最后一次修改 2017-09-19
Clustering
Clustering目前与JDBC-Jobstore(JobStoreTX或JobStoreCMT)和TerracottaJobStore一起使用。功能包括负载平衡和 job故障转移(如果JobDetail的“请求恢复”标志设置为true)。
使用JobStoreTX或JobStoreCMT进行聚类通过将“org.quartz.jobStore.isClustered”属性设置为“true”来启用Clustering。Clustering中的每个实例都应该使用相同的quartz.properties文件。这样做的例外是使用相同的属性文件,具有以下允许的异常:不同的线程池大小,以及“org.quartz.scheduler.instanceId”属性的不同值。Clustering中的每个节点必须具有唯一的instanceId,通过将“AUTO”作为此属性的值,可以轻松完成(不需要不同的属性文件)。
不要在单独的机器上运行Clustering,除非它们的时钟使用某种形式的时间同步服务(守护进程)进行同步,而这些时间同步服务(守护进程)运行非常有限(时钟必须在彼此之间)。 如果您不熟悉如何执行此操作,
不要针对任何其他实例运行的相同的一组表来启动非群集实例。您可能会收到严重的数据损坏,一定会遇到不正常的行为。
每次触发只能有一个节点有效。我的意思是,如果job有一个重复的trigger,告诉它每10秒钟发射一次,那么在12:00:00,正好一个节点将运行这个job,在12:00:10,一个节点将运行job等。它不一定是每次相同的节点 - 它或多或少是随机的,哪个节点运行它。负载平衡机制对于繁忙的调度程序(大量的trigger)来说是近乎随机的,但是有利于于non-busy(例如一个或两个trigger)调度程序活动的同一个节点。
使用TerracottaJobStore进行Clustering简单地将调度程序配置为使用TerracottaJobStore(第9课:JobStores中介绍),并且您的调度程序将全部设置为Clustering。
您可能还需要考虑如何设置Terracotta服务器,特别是打开诸如持久性等功能的配置选项,以及运行一系列用于HA的Terracotta服务器。
TerracottaJobStore的企业版提供了高级的Quartz Where功能,允许将作业的智能定位到适当的Clustering节点。