> 文章列表 > 定时任务:从Timer、STPE、Quartz 到 XXL-JOB

定时任务:从Timer、STPE、Quartz 到 XXL-JOB

定时任务:从Timer、STPE、Quartz 到 XXL-JOB

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor 简称STPE
  • Quartz
  • XXL-JOB

基本套路

定时任务基本上都是在一个while(true)for(;;)死循环中(每次循环判断定时程序是否终止或暂停),从任务存放的地(可以是内存的堆结构,可以是远程数据库获取,可以是阻塞队列)获取最近要执行的任务,获取的最近的任务没到时间就阻塞一段时间,阻塞可以用Object.waitCondition::awaitNanos。对于周期执行的任务,每次执行完毕将下一个周期的自身再次加入任务存放的地方。
除此之外,一个完善的定时框架要考虑执行线程池、如何处理过期任务(顺序执行?丢弃?覆盖?)、重试策略、监听器等方面问题。分布式定时任务还涉及到rpc、任务的分片、执行器的负载均衡执行策略等问题。

Timer

案例:

public static void main(String[] args) {// daemonTimer timer = new Timer(true);// 每秒执行一次timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("timertask running at "+ new Date());}},0,1000);Thread.sleep(10000);
}

源码

Corresponding to each Timer object is a single background thread that is used to execute all of the timer’s tasks, sequentially. If a timer task takes excessive time to complete, it “hogs” the timer’s task execution thread. This can, in turn, delay the execution of subsequent tasks, which may “bunch up” and execute in rapid succession when (and if) the offending task finally completes.
This class is thread-safe: multiple threads can share a single Timer object without the need for external synchronization.
This class does not offer real-time guarantees: it schedules tasks using the Object.wait(long) method.
Java 5.0 introduced ScheduledThreadPoolExecutor which is a versatile replacement for the Timer/TimerTask combination, as it allows multiple service threads, accepts various time units, and doesn’t require subclassing TimerTask (just implement Runnable).
Internally, it uses a binary heap to represent its task queue, so the cost to schedule a task is O(log n), where n is the number of concurrently scheduled tasks.

可以看出,一个Timer中只有一个线程里执行调度任务,所以一个任务执行时间过长会影响后续任务,使用wait方法调度,任务放在一个二叉堆中。不推荐使用。

Timer中包含一个执行线程和一个任务队列:

// 根据nextExecutionTime排序的堆
private final TaskQueue queue = new TaskQueue();
// 传入queue执行
private final TimerThread thread = new TimerThread(queue);

定时任务使用TimerTask代替,TimerTask包含4种状态以及一个period来代表执行策略:

static final int VIRGIN = 0;
static final int SCHEDULED   = 1;
static final int EXECUTED    = 2;
static final int CANCELLED   = 3;// positive value : fixed-rate execution
// negative value : fixed-delay execution
// 0 : non-repeating task
long period = 0;

来看看TimerThread如何安排 TaskQueue中的TimerTask

class TimerThread extends Thread {// 代表是否还有任务,没有会优雅终止调度boolean newTasksMayBeScheduled = true;private TaskQueue queue;public void run() {try {mainLoop();} finally {// Someone killed this Thread, behave as if Timer cancelledsynchronized(queue) {newTasksMayBeScheduled = false;queue.clear();  // Eliminate obsolete references}}}private void mainLoop() {while (true) {try {TimerTask task;boolean taskFired;synchronized(queue) {// 没有任务且newTasksMayBeScheduled为true就挂起while (queue.isEmpty() && newTasksMayBeScheduled)queue.wait();if (queue.isEmpty())break; // Queue is empty and will forever remain; die// Queue nonempty; look at first evt and do the right thinglong currentTime, executionTime;// 最近的任务task = queue.getMin();synchronized(task.lock) {// 是否取消if (task.state == TimerTask.CANCELLED) {queue.removeMin();continue;  // No action required, poll queue again}currentTime = System.currentTimeMillis();executionTime = task.nextExecutionTime;// 设置是否需要执行标志if (taskFired = (executionTime<=currentTime)) {// 只定时就移除if (task.period == 0) { // Non-repeating, removequeue.removeMin();task.state = TimerTask.EXECUTED;} else { // Repeating task, reschedule// 定时重复执行则调整到该任务下一次执行时间,queue会重新堆化queue.rescheduleMin(task.period<0 ? currentTime   - task.period: executionTime + task.period);}}}// 没超时就wait一些时间if (!taskFired) // Task hasn't yet fired; waitqueue.wait(executionTime - currentTime);}// 判断执行if (taskFired)  // Task fired; run it, holding no lockstask.run();} catch(InterruptedException e) {}}}}

可以看到Timer是获取最近时间的定时任务,如果没到时间线程会挂起executionTime - currentTime时间。

ScheduledThreadPoolExecutor (STPE)

案例

	public static void main(String[] args) {ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(3);executor.scheduleAtFixedRate(() -> System.out.println("running at "+ new Date()),0,1, TimeUnit.SECONDS);}

简介

This class is preferable to Timer when multiple worker threads are needed, or when the additional flexibility or capabilities of ThreadPoolExecutor (which this class extends) are required.
Tasks scheduled for exactly the same execution time are enabled in first-in-first-out (FIFO) order of submission. When a submitted task is cancelled before it is run, execution is suppressed. By default, such a cancelled task is not automatically removed from the work queue until its delay elapses. While this enables further inspection and monitoring, it may also cause unbounded retention of cancelled tasks. To avoid this, set setRemoveOnCancelPolicy to true, which causes tasks to be immediately removed from the work queue at time of cancellation.
It acts as a fixed-sized pool using corePoolSize threads and an unbounded queue, adjustments to maximumPoolSize have no useful effect.

可以看出,定时任务线程池设置固定和核心线程数和无界任务队列,最大线程数设置无意义,默认为Integer.MAX_VALUE。另外,提交一个cancelled的任务不会执行,且默认不移除任务队列,当时间过了才移除,可能会导致cancelled任务堆积,可设置setRemoveOnCancelPolicy为true。

任务使用private class ScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V>代表

套路也是一样,创建一个任务加入执行schedule或scheduleAtFixedRate方法时,会创建一个ScheduledFutureTask加入任务队列,然后STPE的父类TPE中会在一个无限循环中从任务队列DelayedWorkQueue中拉取任务执行,关于该任务队列:

A DelayedWorkQueue is based on a heap-based data structure like those in DelayQueue and PriorityQueue, except that every ScheduledFutureTask also records its index into the heap array.
This eliminates the need to find a task upon cancellation, greatly speeding up removal (down from O(n) to O(log n)), and reducing garbage retention that would otherwise occur by waiting for the element to rise to top before clearing.

Timer不同的是,该阻塞队列阻塞用的是JUC的Condition::await那一套,而不是Object::wait;相同的是如果获取到的任务还没到达时间,同样也是需要挂起一段时间,挂起用的是Conditioni::awaitNanos,参看如下代码:

static class DelayedWorkQueue extends AbstractQueue<Runnable>implements BlockingQueue<Runnable> {public RunnableScheduledFuture<?> take() throws InterruptedException {final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {for (;;) {RunnableScheduledFuture<?> first = queue[0];// 队列为空则挂起if (first == null)available.await();// 不为空else {// 获取第一个要执行的任务的执行还剩多少时间long delay = first.getDelay(NANOSECONDS);// 不剩时间 阻塞队列直接返回if (delay <= 0)return finishPoll(first);first = null; // don't retain ref while waiting// Thread designated to wait for the task at the head of the queue. This variant of the Leader-Follower pattern serves to minimize unnecessary timed waiting.				// leader不为空则挂起if (leader != null)available.await();else {Thread thisThread = Thread.currentThread();// leader为空则该线程设为leaderleader = thisThread;try {// **********// 挂起相应时间// **********available.awaitNanos(delay);} finally {// 清空if (leader == thisThread)leader = null;}}}}} finally {// 唤醒followerif (leader == null && queue[0] != null)available.signal();lock.unlock();}}}

这里有一个 Leader-Follower Pattern,可参考Explain “Leader/Follower” Pattern
执行完了会将下一个周期要执行的新任务加入任务队列,参见ScheduledFutureTask::run和::reExecutePeriodic

    private class ScheduledFutureTask<V>extends FutureTask<V> implements RunnableScheduledFuture<V> {...public void run() {// 是否周期执行boolean periodic = isPeriodic();// 判断线程池是否关闭,关闭就不执行if (!canRunInCurrentRunState(periodic))cancel(false);// 不是周期方法,则直接执行else if (!periodic)ScheduledFutureTask.super.run();// 周期方法执行,并更新下一轮时间,加入任务队列else if (ScheduledFutureTask.super.runAndReset()) {setNextRunTime();// 重新加入任务队列reExecutePeriodic(outerTask);}}...}

可以看到STPETimer策略一样,获取最近时间的定时任务,如果没到时间线程会挂起executionTime - currentTime时间,不过这个步骤在Timer中是由执行线程TimerThread完成,而不是存储任务的TaskQueue,而在STPE中,该步骤放在了阻塞队列DelayedWorkQueue中完成。

Quartz

案例

	// Jobstatic class TestJob implements Job {@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException {System.out.println("TestJob executing~");}}public static void main(String[] args) {// SchedulerScheduler defaultScheduler = StdSchedulerFactory.getDefaultScheduler();defaultScheduler.start();// JobDetailJobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity("testJob", "default").build();// TriggerTriggerBuilder<CronTrigger> triggerBuilder = TriggerBuilder.newTrigger().withIdentity("trigger", "default").startNow().withSchedule(CronScheduleBuilder.cronSchedule("0 * * * * ?"));CronTrigger cronTrigger = triggerBuilder.build();defaultScheduler.scheduleJob(jobDetail,cronTrigger);}

简介

  • Job: 任务,通常用户实现Job接口,业务相关
  • JobDetail: 任务详情,除了包含业务内容Job之外,还有其他信息,如key datamap description等
  • Trigger: 触发器,任务如何触发,如CronTrigger
  • Scheduler: 调度器门面,大管家,协调JobDetail和Trigger,常见的StdScheduler
  • SchedulerFactory: 调度器工厂
  • JobStore: 提供Job和Trigger存储,常见的RAMJobstore内存存储
  • QuartzScheduler : StdScheduler代理调度器中真正的调度器
  • QuartzSchedulerResources: QuartzScheduler中资源管家,提供线程池、JobStore等资源
  • QuartzSchedulerThread: 调度线程,作用类似Timer中的TimerThread
  • 各种监听器: JobListener TriggerListener SchedulerListener

主要的流程都集中在QuartzSchedulerThread::run中,源码略。
可以看到QuartzSchedulerThreadQuartzSchedulerResources.getJobStore().acquireNextTriggers(...)获取一段时间内可以执行的任务,获取失败第一次会通知监听器,后面会有指数退避,但不会通知监听器。后面先判断是否到了运行时间,如果离运行时间在2ms内则直接运行,否则调用Object::wait方法挂起waitTime - now。如果不用挂起,则初始化运行结果TriggerFiredResult,结果中获取TriggerFiredBundle,将TriggerFiredBundle转化为JobRunShell,最后实际运行QuartzSchedulerResources.getThreadPool().runInThread(shell)

QuartzSchedulerResources.getJobStore().acquireNextTriggers(...)中获取任务,其中也会做任务是否过期的判断,如RAMJobStore中:

 public List<OperableTrigger> acquireNextTriggers(long noLaterThan, int maxCount, long timeWindow) {...// 判断任务是否过期if (applyMisfire(tw)) {if (tw.trigger.getNextFireTime() != null) {timeTriggers.add(tw);}continue;}protected boolean applyMisfire(TriggerWrapper tw) {long misfireTime = System.currentTimeMillis();if (getMisfireThreshold() > 0) {misfireTime -= getMisfireThreshold();}Date tnft = tw.trigger.getNextFireTime();// tw.trigger.getMisfireInstruction() Trigger接口中有两种// MISFIRE_INSTRUCTION_SMART_POLICY 根据updateAfterMisfire()判读但// MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY 忽略// CronTrigger还有两种:// MISFIRE_INSTRUCTION_FIRE_ONCE_NOW// MISFIRE_INSTRUCTION_DO_NOTHING// SimpleTrigger还有两种:// MISFIRE_INSTRUCTION_FIRE_NOW// MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNTif (tnft == null || tnft.getTime() > misfireTime || tw.trigger.getMisfireInstruction() == Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY) { return false; }...}}

Quartz没有可执行的任务会使用Object::wait方法挂起,整个加锁和挂起等待用的是synchronized和Object::wait那一套,和Timer类似。不同的是,Quartz每次获取的是一段时间内可执行的一批任务。

分布式调度平台XXL-JOB

定时任务:从Timer、STPE、Quartz 到 XXL-JOB

XXL-JOB 分布式任务调度平台

分布式调用平台涉及分布式环境的交互,所以rpc是不可避免地,XXL-JOB中使用netty框架构建了基于HTTP的rpc交互方式(XXL-RPC)

  • xxl-job-admin:调度中心(基于SpringBoot)
  • xxl-job-core:公共依赖(客户端依赖)

其中,调度中心是一个前后端不分离、完整的Spring Web项目,完成任务调度,xxl-job-admin同时依赖xxl-job-core。而每个执行器(任务执行的客户端,即业务集群节点)也需要添加依赖xxl-job-core,在执行器端的该依赖中,XXL基于Netty构建了自己的http接收方式来接收来自调度中心的各种请求,如:任务执行、终止、日志请求,参见EmbedServerEmbedHttpServerHandler;执行器客户端在启动时也要向调度中心注册该执行器(客户端集群节点信息,非任务信息,BEAN模式下任务维护在执行器端的ConcurrentHashMap中,不向调度中心注册),同时调度中心会通过Java原生http向各个执行器发送心跳来检测是否存活以及检测任务执行状态,执行器执行将结果放入一个结果阻塞队列,线程从队列中获取结果向调度中心发送消息,该发送过程也是通过Java原生http完成的,调度中心和执行发送http请求共用XxlJobRemotingUtil工具类来完成。

从以上的描述可以看出:

调度中心 执行器
http发送方式 java原生http java原生http
http接收方式 Spring Web Netty

调度中心用于和执行器交互的api(Spring Web构建)只有3个,可视化平台还有其他的不做讨论:

  • /api/callback : 执行器执行回调任务结果
  • /api/registry : 执行器注册和心跳检测
  • /api/registryRemove : 执行器注册摘除时使用,注册摘除后的执行器不参与任务调度与执行

执行器的api(netty构建)有5个:

  • /beat : 调度中心Failover路由模式下主动检测执行器是否在线
  • /idleBeat : 调度中心检测指定执行器上指定任务是否忙碌(运行中)
  • /run : 触发任务执行
  • /kill : 终止任务
  • /log : 终止任务,滚动方式加载

注意,心跳检测是执行器向调度中心每隔30s向*/api/registry*发送心跳(参见ExecutorRegistryThread),该api同样也是注册接口;而执行器端的 /beat是调度中心在Failover路由模式下向执行器分配任务时主动检测时用的,只能说作者api命名地太抽象了🙈。

调度中心调度同样和上面地几个调度方式差不多,XXL-JOB在一个while(!stop)循环中,向数据库查询一段时间内(默认接下来的5000ms)地任务, 然后判断任务是否过期,如果过期超过5000ms要看你的过期策略是什么;如果过期没超过5000ms就立即执行,且看看你下次执行是否又在下一个5000ms内,在的话加入时间轮;时间没到也加入时间轮。时间轮中存放key和对应的List<jobInfo.getId()>,其中key=((jobInfo.getTriggerNextTime()/1000)%60),然后另外有一个线程每隔一秒拿出里面的List<jobInfo.getId()>遍历获取任务分配给执行器执行,分配路由策略有多种:

  • Busyover
  • ConsistentHash
  • Failover
  • First
  • Last
  • LFU
  • LRU
  • Random
  • Round

执行完了要更新任务下一轮触发时间更新数据库,包括任务执行结果、日志、下一轮任务trigger时间等。以上代码主要集中在JobScheduleHelper类中。

可以看到在任务时间没到之前,XXL-JOB不像之前的定时任务那样采用线程挂起的方式,而是使用时间轮存储任务id,另起一个线程每隔一秒从时间轮获取当前时间的任务id来分配给执行器执行。