前言
linux中的cron可以是任务调度的初形。随着任务的复杂性及分布式需求,cron显的力不从心。Quartz是经典的作业调度框架。
Quartz的基础
在深入研究之初,先仿照quartz的原理,设计一个简单的作业调度器。
- 先看job类,这个类,非常简单,只有一个execute方法,该方法是job具体执行的内容:
public class Job {
public void execute(Map<String, String> jobData) {
System.out.println("定时器任务执行" + new Date(System.currentTimeMillis()));
System.out.println("参数值"+jobData.get("jobName"));
}
}
- jobdetail类,该类是对具体job类的封装,包括jobName(任务标识),job执行需要的运行时参数jobdata
public class JobDetail {
private Class<? extends Job> clz;
private String jobName;
private Map<String, String> jobData;
public JobDetail(Class<? extends Job> clz, String jobName) {
this.clz = clz;
this.jobName = jobName;
this.jobData = new HashMap<>();
this.jobData.put("jobName", this.jobName);
}
public String getJobName(){
return this.jobName;
}
public void executeJob(){
try {
clz.newInstance().execute(jobData);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- trigger类,记录下次运行作业的时间和运行job的key
public class Trigger implements Comparable<Trigger>{
private String jobKey;
private long nextFireTime;
public Trigger(long nextFireTime) {
this.nextFireTime = nextFireTime;
}
// 用于根据触发时间排序
@Override
public int compareTo(Trigger o) {
return (int)( this.nextFireTime - o.nextFireTime);
}
public void setJobKey(String jobKey){
this.jobKey = jobKey;
}
public String getJobKey() {
return jobKey;
}
public long getNextFireTime() {
return nextFireTime;
}
}
- scheduler类,最重要的类,用来启动和停止框架
public class Scheduler {
private Map<String, JobDetail> jobMap = new HashMap<>();
private TreeSet<Trigger> triggerList = new TreeSet<>();
private SchedulerThread schedulerThread = new SchedulerThread();
public void schedulerJob(JobDetail jobDetail, Trigger trigger){
jobMap.put(jobDetail.getJobName(),jobDetail);
trigger.setJobKey(jobDetail.getJobName());
triggerList.add(trigger);
}
public void start(){
schedulerThread.start();
}
public void stop(){
schedulerThread.halt();
}
}
- scheduler的执行是在scheduler的schedulerThread中执行
public class SchedulerThread extends Thread {
private boolean shutDown = false;
@Override
public void run() {
while (!shutDown){
Trigger trigger = triggerList.pollFirst();
if(trigger == null){
try {Thread.sleep(100);} catch (InterruptedException e) {}
continue;
}
long cur = System.currentTimeMillis();
long next = trigger.getNextFireTime();
if(cur < next){
try {Thread.sleep(next - cur);} catch (InterruptedException e) {}
}
JobDetail jobDetail = jobMap.get(trigger.getJobKey());
jobDetail.executeJob();
}
}
public void halt(){
shutDown = true;
}
}
至此所有的框架代码都已经完成,写个main测试下
public static void main(String[] args) {
Scheduler scheduler = new Scheduler();
scheduler.schedulerJob(new JobDetail(Job.class, "job01"), new Trigger(System.currentTimeMillis() + 1000));
scheduler.schedulerJob(new JobDetail(Job.class, "job02"), new Trigger(System.currentTimeMillis() + 2000));
scheduler.start();
}
上述的设计,可以说Qz是去除安全验证和多线程同步问题编写的基本调度任务。下面来看真实案例
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("定时器任务执行" + new Date(System.currentTimeMillis()));
JobDataMap map=jobExecutionContext.getMergedJobDataMap();
System.out.println("参数值"+map.get("uname"));
}
public static void main(String[] args) throws Exception {
//1.创建Scheduler的工厂
SchedulerFactory sf = new StdSchedulerFactory();
//2.从工厂中获取调度器实例
Scheduler scheduler = sf.getScheduler();
//3.创建JobDetail(作业信息)
JobDetail jb = JobBuilder.newJob(MyJob.class)
.withDescription("this is a test job") //job的描述
.withIdentity("testJob", "testGroup") //job 的name和group
.build();
//向任务传递数据
JobDataMap jobDataMap = jb.getJobDataMap();
jobDataMap.put("uname", "张三");
//任务运行的时间,SimpleSchedle类型触发器有效
long time = System.currentTimeMillis() + 3 * 1000L; //3秒后启动任务
Date statTime = new Date(time);
//4.创建Trigger
//使用SimpleScheduleBuilder或者CronScheduleBuilder
Trigger t = TriggerBuilder.newTrigger()
.withDescription("")
.withIdentity("ramTrigger", "ramTriggerGroup")
.startAt(statTime) //默认当前时间启动
//普通计时器
//.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).withRepeatCount(3))//间隔3秒,重复3次
//表达式计时器
.withSchedule(CronScheduleBuilder.cronSchedule("0/3 * * * * ?")) //3秒执行一次
.build();
//5.注册任务和定时器
scheduler.scheduleJob(jb, t);
//6.启动 调度器
scheduler.start();
}
}
Quartz的分布式
关于分布式调度的二个核心问题:
- Quartz 如何向每台服务器分配资源
- Quartz怎么保证在一台挂机的情况下,保证任务不丢失
JobStore
Quartz将调度程序的所有的工作数据包括:Jobs,triggers,日历等信息,交由JobStore管理。JobStore的存储方式分为RAM和JDBC两种。群集模式下使用的JDBC。
下面是分析QuartzSchedulerThread源码解决Quartz 如何向每台服务器分配资源,中JobStore的主要作用函数
public interface JobStore {
// 此方法主要是申请将要执行任务:
// 1. 从triggers表中拉取状态为 WAITING 的 Trigger
// 2. Trigger状态从WAITING改为 ACQUIRED,
// 3. 如果2修改成功,则向 FIRED_TRIGGERS 表插入记录
// 此方法有两种方式保证任务的分配
// 1. 乐观锁,即任务从WAITING改为 ACQUIRED失败,则表示被别台机子分配,也可能ABA问题引起任务重复执行
// 2. 悲观锁, 步骤1前,通过Lock表进行上锁
List<OperableTrigger> acquireNextTriggers(long noLaterThan, int maxCount, long timeWindow);
// 此方法主要作用在Trigger的触发:
// 1. 将Trigger状态ACQUIRED, 将 FIRED_TRIGGERS状态改为 EXECUTING
// 2. 计算trigger的NEXT_FIRE_TIME
// 3. 如果 NEXT_FIRE_TIME 为空,trigger状态 变成 STATE_COMPLETE, 并不进行4步骤
// 4. 如果不允许并发执行,将 trigger状态 变成 BLOCKED, 否则 置为wait (这里就是导致上面ABA问题的主要原因)
List<TriggerFiredResult> triggersFired(List<OperableTrigger> triggers);
// 通过shellJob结果监听器notifyJobStoreJobComplete进行回调
// 1. 如果任务正常, 则将FIRED_TRIGGERS记录删除
void triggeredJobComplete(OperableTrigger trigger, JobDetail jobDetail, Trigger.CompletedExecutionInstruction triggerInstCode);
}
下面是分析ClusterManager源码解决Quartz 保证任务不丢失
- 项目启动时,会这 SCHEDULER_STATE 表 插入数据
- 之后维护和 SCHEDULER_STATE 的心跳
- 如果发布哪台服务器掉线,则重置任务继续执行 @see JobStoreSupport.clusterRecover
Quartz 架构
经上面分析,我们可以得出Quartz的大致结构图