任务调度的实现方式有很多,如果要实现我们的调度需求,我们对这个工具有什么样的基本要求呢?
1.可以定义触发的规则,比如基于时刻、时间间隔、表达式。
2.可以定义需要执行的任务。比如执行一个脚本或者一段代码。任务和规则是分开的。
3.集中管理配置,持久配置。不用把规则写在代码里面,可以看到所有的任务配置,方便维护。重启之后任务可以再次调度——配置文件或者配置中心。
4.支持任务的串行执行,例如执行 A 任务后再执行 B 任务再执行 C 任务。
5.支持多个任务并发执行,互不干扰(例如 ScheduledThreadPoolExecutor)。
6.有自己的调度器,可以启动、中断、停止任务。
7.容易集成到 Spring。
单线程执行
import java.util.Timer;
import java.util.TimerTask;
public class TestTimerTask extends TimerTask {
/**
* 此计时器任务要执行的操作。
*/
public void run() {
Date executeTime = new Date(this.scheduledExecutionTime());
String dateStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
System.out.println("任务执行了:" + dateStr);
}
}
public class TestTimer {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task = new TestTimerTask();
timer.schedule(task, 5000L, 1000L);
}
}
官网:http://www.quartz-scheduler.org/
Quatz 是一个特性丰富的,开源的任务调度库,它几乎可以嵌入所有的 Java 程序,从很小的独立应用程序到大型商业系统。Quartz 可以用来创建成百上千的简单的或者复杂的任务,这些任务可以用来执行任何程序可以做的事情。
Quartz 拥有很多企业级的特性,包括支持 JTA 事务和集群。
Quartz 是一个老牌的任务调度系统,98 年构思,01 年发布到 sourceforge。现在更新比较慢,因为已经非常成熟了。https://github.com/quartz-scheduler/quartz
Quartz 的目的就是让任务调度更加简单,开发人员只需要关注业务即可。他是用 Java 语言编写的(也有.NET 的版本)。Java 代码能做的任何事情,Quartz 都可以调度。
特点:
精确到毫秒级别的调度
可以独立运行,也可以集成到容器中
支持事务(JobStoreCMT )
支持集群
支持持久化
我们创建一个实现 Job 接口的类,使用 JobBuilder 包装成 JobDetail,它可以携带KV 的数据。
定义任务的触发规律,Trigger,使用 TriggerBuilder 来构建。
JobDetail 跟 Trigger 是 1:N 的关系。
思考:为什么要解耦?
Trigger 接口在 Quartz 有 4 个继承的子接口:
MutableTrigger 和 CoreTrigger 最终也是用到以上四个类的实现类。
SimpleTrigger 可以定义固定时刻或者固定时间间隔的调度规则(精确到毫秒)。
例如:每天 9 点钟运行;每隔 30 分钟运行一次。
CalendarIntervalTrigger 可以定义更多时间单位的调度需求,精确到秒。
好处是不需要去计算时间间隔,比如 1 个小时等于多少毫秒。
例如每年、每个月、每周、每天、每小时、每分钟、每秒。
每年的月数和每个月的天数不是固定的,这种情况也适用。
每天的某个时间段内,以一定的时间间隔执行任务。
例如:每天早上 9 点到晚上 9 点,每隔半个小时执行一次,并且只在周一到周六执
行。
CronTirgger 可以定义基于 Cron 表达式的调度规则,是最常用的触发器类型
上面我们定义的都是在什么时间执行,但是我们有一些在什么时间不执行的需求,
比如:理财周末和法定假日购买不计息;证券公司周末和法定假日休市。
是不是要把日期写在数据库中,然后读取基于当前时间判断呢?
基于 Calendar 的排除规则
如果要在触发器的基础上,排除一些时间区间不执行任务,就要用到 Quartz 的
Calendar 类(注意不是 JDK 的 Calendar)。可以按年、月、周、日、特定日期、Cron表达式排除
调用 Trigger 的 modifiedByCalendar()添加到触发器中,并且调用调度器的
addCalendar()方法注册排除规则
public class CalendarDemo {
public static void main(String[] args) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
scheduler.start();
// 定义日历
AnnualCalendar holidays = new AnnualCalendar();
// 排除日
Calendar gupaoDay = (Calendar) new GregorianCalendar(2019, 8, 8);
holidays.setDayExcluded(gupaoDay, true);
// 排除中秋节
Calendar midAutumn = new GregorianCalendar(2019, 9, 13);
holidays.setDayExcluded(midAutumn, true);
// 排除圣诞节
Calendar christmas = new GregorianCalendar(2019, 12, 25);
holidays.setDayExcluded(christmas, true);
// 调度器添加日历
scheduler.addCalendar("holidays", holidays, false, false);
JobDetail jobDetail = JobBuilder.newJob(MyJob1.class)
.withIdentity("job1", "group1")
.usingJobData("gupao","测试 2673")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.modifiedByCalendar("holidays")
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();
Date firstRunTime = scheduler.scheduleJob(jobDetail, trigger);
System.out.println(jobDetail.getKey() + " 第一次触发: " + firstRunTime);
}
}
调度器,是 Quartz 的指挥官,由 StdSchedulerFactory 产生。它是单例的。
并且是 Quartz 中最重要的 API,默认是实现类是 StdScheduler,里面包含了一个
QuartzScheduler。QuartzScheduler 里面又包含了一个 QuartzSchedulerThread。
Scheduler 中的方法主要分为三大类:
1)操作调度器本身,例如调度器的启动 start()、调度器的关闭 shutdown()。
2)操作 Trigger,例如 pauseTriggers()、resumeTrigger()。
3)操作 Job,例如 scheduleJob()、unscheduleJob()、rescheduleJob()
这些方法非常重要,可以实现任务的动态调度
我们有这么一种需求,在每个任务运行结束之后发送通知给运维管理员。那是不是
要在每个任务的最后添加一行代码呢?这种方式对原来的代码造成了入侵,不利于维护。
如果代码不是写在任务代码的最后一行,怎么知道任务执行完了呢?或者说,怎么监测
到任务的生命周期呢?
观察者模式:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则
所有依赖它的对象都会得到通知并自动更新。
Quartz 中提供了三种 Listener,监听 Scheduler 的,监听 Trigger 的,监听 Job 的。
只需要创建类实现相应的接口,并在 Scheduler 上注册 Listener,便可实现对核心
对象的监听。
SchedulerListener
方法比较多,省略
问题:最多可以运行多少个任务(磁盘、内存、线程数)
Jobstore 用来存储任务和触发器相关的信息,例如所有任务的名称、数量、状态等
等。Quartz 中有两种存储任务的方式,一种在在内存,一种是在数据库。
Quartz 默认的 JobStore 是 RAMJobstore,也就是把任务和触发器信息运行的信息存储在内存中,用到了 HashMap、TreeSet、HashSet 等等数据结构。
如果程序崩溃或重启,所有存储在内存中的数据都会丢失。所以我们需要把这些数据持久化到磁盘。
JDBCJobStore 可以通过 JDBC 接口,将任务运行数据保存在数据库中
JDBC 的实现方式有两种,JobStoreSupport 类的两个子类:
JobStoreTX:在独立的程序中使用,自己管理事务,不参与外部事务。
JobStoreCMT:(Container Managed Transactions (CMT),如果需要容器管理事务时,使用它。
使用 JDBCJobSotre 时,需要配置数据库信息:
org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 使用 quartz.properties,不使用默认配置
org.quartz.jobStore.useProperties:true
#数据库中 quartz 表的表名前缀
org.quartz.jobStore.tablePrefix:QRTZ_
org.quartz.jobStore.dataSource:myDS
#配置数据源
org.quartz.dataSource.myDS.driver:com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL:jdbc:mysql://localhost:3306/gupao?useUnicode=true&characterEncoding=utf8
org.quartz.dataSource.myDS.user:root
org.quartz.dataSource.myDS.password:123456
org.quartz.dataSource.myDS.validationQuery=select 0 from dual
问题来了?需要建什么表?表里面有什么字段?字段类型和长度是什么?
在官网的 Downloads 链接中,提供了 11 张表的建表语句:quartz-2.2.3-distribution\quartz-2.2.3\docs\dbTables
2.3 的版本在这个路径下:src\org\quartz\impl\jdbcjobstore