java中执行定时任务的6种姿式

定时任务的场景

所谓定时任务实际上有两种状况, 一种是在某个特定的时间点触发执行某个任务, 例如天天凌晨, 每周六下午2点等等. 另一种是以特定的间隔或频率触发某个任务,例如每小时触发一次等.html

在咱们实际的工做中,用到定时任务的场景是很是的多的,例如:java

  1. 天天统计某些业务数据, 作报表展现
  2. 用户下单后, 30分钟未支付则取消订单
  3. 特定的时间点给用户发送消息(祝福短信等)
  4. 补偿机制, 按期扫描数据库和日志,对比差别的数据并进行补偿
  5. 等等...

正是由于应用场景很是的广, 因此前辈程序员们也是绞尽脑汁, 为咱们创造了不少实用用的工具和框架, 咱们今天才得以站在巨人的肩膀上,看得更远,走得更快.linux

下面咱们就来列举一下这些方法和工具.程序员

crontab

crontab严格来讲并非属于java内的. 它是linux自带的一个工具, 能够周期性地执行某个shell脚本或命令.shell

可是因为crontab在实际开发中应用比较多, 并且crontab表达式跟咱们后面介绍的其余定时任务框架的cron表达式是相似的, 因此这里仍是最早介绍crontab数据库

crontab的用法是:json

crontabExpression command
复制代码

首先, command能够是一个linux命令(例如echo 123), 或一个shell脚本(例如 test.sh), 也能够是二者结合(例如: cd /tmp; sh test.sh)api

crontabExpression大概是长下面这样子bash

# 每小时的第5分钟执行一次命令
5 * * * * Command 
# 指定天天下午的 6:30 执行一次命令
30 18 * * * Command 
# 指定每个月8号的7:30分执行一次命令
30 7 8 * * Command
# 指定每一年的6月8日5:30执行一次命令
30 5 8 6 * Command 
# 指定每星期日的6:30执行一次命令
30 6 * * 0 Command 
复制代码

其中crontabExpression一共有5列, 含义以下:服务器

  1. 第一列表示是分钟, 取值为0-59
  2. 第二列表示是时, 取值为0-59
  3. 第三列表示是日
  4. 第四列表示是月, 取值是0-12
  5. 第5列表示是星期

此外, 每列还能够是* ? -等等特殊字符, 具体含义能够参考这篇文章, 里面总结得比较好, 我这里就再也不多说了

timer

即jdk里面提供的java.util.Timer和java.util.TimerTask两个类.

其中TimerTask表示具体的任务,而Timer调度任务.

简单的例子以下:

import java.util.Timer;
import java.util.TimerTask;

public class TimerTest extends TimerTask {

    private String jobName = "";

    public TimerTest(String jobName) {
        super();
        this.jobName = jobName;
    }

    @Override
    public void run() {
        System.out.println("execute " + jobName);
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        long delay1 = 1 * 1000;
        long period1 = 1000;
        // 从如今开始 1 秒钟以后,每隔 1 秒钟执行一次 job1
        timer.schedule(new TimerTest("job1"), delay1, period1);
        long delay2 = 2 * 1000;
        long period2 = 2000;
        // 从如今开始 2 秒钟以后,每隔 2 秒钟执行一次 job2
        timer.schedule(new TimerTest("job2"), delay2, period2);
    }
}
复制代码

固然在生产环境中Timer是不建议使用了的. 它在多线程的环境下, 会存在必定的问题:

1. 当一个线程抛出异常时,整个timer都会中止运行.例如上面的job1抛出异常的话, 
job2也不会再跑了.
2. 当一个线程里面处理的时间很是长的话, 会影响其余job的调度. 
例如, 若是job1处理的时间要60秒的话, 那么job2就变成了60秒跑一次了.
复制代码

基于上面的缘由, timer如今通常都不会再使用了.

ScheduledExecutorService

ScheduledExecutorService 就是JDK里面自定义的几种线程池中的一种.

从API上看, 感受它就是用来替代Timer的,并且彻底能够替代的. 只是不知道为什么Timer仍是没有被标记为过时, 想必是还有一些应用的场景吧

首先, Timer能作到的事情ScheduledExecutorService都能作到;

其次, ScheduledExecutorService能够完美的解决上面所说的Timer存在的两个问题:

1. 抛异常时, 即便异常没有被捕获, 线程池也还会新建线程, 因此定时任务不会中止

2. 因为ScheduledExecutorService是不一样线程处理不一样的任务, 所以,无论一个线程的运行时间有多长, 都不会影响到另一个线程的运行.
复制代码

固然, ScheduledExecutorService也不是万能的. 例如若是我想实现"在每周六下午2点"执行某行代码这个需求时, ScheduledExecutorService实现起来就有点麻烦了.

ScheduledExecutorService更适合调度这些简单的以特定频率执行的任务.其余的, 就要轮到咱们大名鼎鼎的quartz上场了.

quartz

在java的世界里, quartz绝对是总统山级别的王者的存在. 市面上大多数的开源的调度框架也基本都是直接或间接基于这个框架来开发的.

先来看经过一个最简单的quartz的例子, 来简单地认识一下它.

使用cron表达式来让quartz每10秒钟执行一个任务:

先引入maven依赖:

<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
复制代码

编写代码:

import com.alibaba.fastjson.JSON;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

public class QuartzTest implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("这里是你的定时任务: " + JSON.toJSONString( jobExecutionContext.getJobDetail()));
    }


    public static void main(String[] args) {
        try {
            // 获取到一个StdScheduler, StdScheduler实际上是QuartzScheduler的一个代理
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 启动Scheduler
            scheduler.start();
            // 新建一个Job, 指定执行类是QuartzTest(需实现Job), 指定一个K/V类型的数据, 指定job的name和group
            JobDetail job = newJob(QuartzTest.class)
                    .usingJobData("jobData", "test")
                    .withIdentity("myJob", "group1")
                    .build();
            // 新建一个Trigger, 表示JobDetail的调度计划, 这里的cron表达式是 每10秒执行一次
            Trigger trigger = newTrigger()
                    .withIdentity("myTrigger", "group1")
                    .startNow()
                    .withSchedule(cronSchedule("0/10 * * * * ?"))
                    .build();


            // 让scheduler开始调度这个job, 按trigger指定的计划
            scheduler.scheduleJob(job, trigger);


            // 保持进程不被销毁
           //  scheduler.shutdown();
            Thread.sleep(10000000);

        } catch (SchedulerException se) {
            se.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

复制代码

上面这个简单的例子已经包含了quartz的几个核心组件:

Scheduler - 能够理解为是一个调度的实例,用来调度任务
Job - 这个是一个接口, 表示调度要执行的任务. 相似TimerTask.
JobDetail - 用于定义做业的实例。进一步封装和拓展Job的具体实例
Trigger(即触发器) - 定义JobDetail的调度计划。例如多久执行一次, 何时执行, 以什么频率执行等等
JobBuilder - 用于定义/构建JobDetail实例。
TriggerBuilder - 用于定义/构建触发器实例。
复制代码
1. Scheduler

Scheduler是一个接口, 它一共有4个实现:

JBoss4RMIRemoteMBeanScheduler
RemoteMBeanScheduler
RemoteScheduler
StdScheduler
复制代码

咱们上面的例子使用的是StdScheduler, 表示的直接在本地进行调度(其余的都带有remote字样, 明显是跟远程调用有关).

来看一下StdScheduler的注释和构造方法

/**
 * <p>
 * An implementation of the <code>Scheduler</code> interface that directly
 * proxies all method calls to the equivalent call on a given <code>QuartzScheduler</code>
 * instance.
 * </p>
 * 
 * @see org.quartz.Scheduler
 * @see org.quartz.core.QuartzScheduler
 *
 * @author James House
 */
public class StdScheduler implements Scheduler {

    /**
     * <p>
     * Construct a <code>StdScheduler</code> instance to proxy the given
     * <code>QuartzScheduler</code> instance, and with the given <code>SchedulingContext</code>.
     * </p>
     */
    public StdScheduler(QuartzScheduler sched) {
        this.sched = sched;
    }
}
复制代码

原来StdScheduler只不过是一个代理而已, 它最终都是调用org.quartz.core.QuartzScheduler类的方法.

查看RemoteScheduler等另外三个的实现, 也都是代理QuartzScheduler而已.

因此很明显, quartz的核心是QuartzScheduler类.

因此来看一下QuartzScheduler的javadoc注释:

/**
 * <p>
 * This is the heart of Quartz, an indirect implementation of the <code>{@link org.quartz.Scheduler}</code>
 * interface, containing methods to schedule <code>{@link org.quartz.Job}</code>s,
 * register <code>{@link org.quartz.JobListener}</code> instances, etc.
 * </p>
 * 
 * @see org.quartz.Scheduler
 * @see org.quartz.core.QuartzSchedulerThread
 * @see org.quartz.spi.JobStore
 * @see org.quartz.spi.ThreadPool
 * 
 * @author James House
 */
public class QuartzScheduler implements RemotableQuartzScheduler {
	...
}
复制代码

大概意思就是说: QuartzScheduler是quartz的心脏, 间接实现了org.quartz.Scheduler接口, 包含了调度Job和注册JobListener的方法等等

说是间接实现说Scheduler接口,可是来看一下它的继承图, 你会发现它跟Scheduler接口没有半毛钱关系(果真够间接的), 彻底是本身独立搞了一套, 基本全部调度相关的逻辑都在里面实现了

image

另外从这个继承图中的RemotableQuartzScheduler也能够看出, QuartzScheduler是天生就能够支持远程调度的(经过rmi远程触发调度, 调度的管理和调度的执行能够分离).

固然, 实际应用中也大多数都是这么用, 只是咱们这个最简单的例子是本地触发调度,本地执行任务而已.

2. Job, JobDetail

Job是一个接口, 它只定义了一个execute方法, 表明任务执行的逻辑.

public interface Job {
    void execute(JobExecutionContext context)
        throws JobExecutionException;
}
复制代码

JobDetail其实也是一个接口, 它的默认实现是JobDetailImpl.JobDetail内部指定了JobDetail的实现类, 另外还新增了一些参数:

1. name和group, 会组合成一个JobKey对象, 做为这个JobDetail的惟一标识ID
2. jobDataMap, 能够给Job传递一些额外参数
3. durability, 是否须要持久化.这就是quartz跟通常的Timer之流不同的地方了. 他的job是能够持久化到数据库的
复制代码

能够看的出来, JobDetail实际上是对Job类的一种加强. Job用来表示任务的执行逻辑, 而JobDetail更多的是跟Job管理相关.

3. Trigger

Trigger接口能够说才是quartz的核心功能. 由于quartz是一个定时任务调度框架, 而定时任务的调度逻辑, 就是在Trigger中实现的.

来看一下Trigger的实现类, 乍一看还挺多. 可是实际就图中红圈圈出来的那几个是真正的实现类, 其余的都是接口或实现类:

image

而实际上, 咱们用得最多的也只是SimpleTriggerImpl和CronTriggerImpl, 前者表示简单的调度逻辑,例如每1分钟执行一次. 后者可使用cron表达式来 指定更复杂的调度逻辑.

很明显, 上面简单的例子咱们用的是CronTriggerImp

不过须要注意的是, quartz的cron表达式和linux下crontab的cron表达式是有必定区别的, 它能够直接到秒级别:

1. Seconds
2. Minutes
3. Hours
4. Day-of-Month
5. Month
6. Day-of-Week
7. Year (optional field)

例如: "0 0 12?* WED" - 这意味着"每一个星期三下午12:00"复制代码

使用CronTrigger的时候, 直接写cron表达式是比较容易出错的, 因此最好有个工具验证一下本身的cron表达式是否写正确, 以及验证触发的时间是不是咱们期待的.

这个工做已经有人帮咱们作好了, 例以下面这个网站:

tool.lu/crontab/

实际效果以下:

iamge

以上就算是quartz的一个入门教程了. 可是确实也只是一个入门教程而已.实际上quartz远比这个例子表现出来的复杂, 也同时也远比这个例子体现出来的强大.

例如:

1. quartz能够配置成集群模式,能够提供失败转移,负载均衡等功能, 在提高计算能力的同时,也提高了系统的可用性
2. quartz还支持JTA事务, 能够将一些job运行在一个事务中
3. 只要服务器资源上能支持, quartz理论上能运行成千上万的job
4. 等等等...
复制代码

固然, quartz也不是没有缺点; 整个框架的重点都是在于"调度"上,而忽略了一些其余的方面, 例如交互和性能.

  1. 交互上, quartz只是提供了"scheduler.scheduleJob(job, trigger)" 这种api的方式. 没有提供任何的管理界面,这是很是的不人性化的.

  2. quartz并无原生地支持分片的功能.这会致使运行一个大的任务时, 运行时间会很是的长. 例如要跑一亿个会员的数据时, 有可能一天都跑不完.若是是支持分片的那就好办不少了.能够把一亿会员拆分到多个实例上跑, 性能更高.

在这两点上, 一些其余的框架作得就更好了.

elastic-job 和 xxlJob

elastic-job和xxl-job是两个很是优秀的分布式任务调度框架, 在我使用过的全部分布调度框架中, 这两个框架起码能排前2位(由于我就用过这两个, 哈哈哈)

这两个框架各有各的特色, 其中共同点都有: 分布式, 轻量级, 交互人性化

elastic-job

elastic-job是当当基于quartz二次开发而开源的一个分布式框架, 功能十分强大. 但在我使用的经验来看, elastic-job最大的亮点有两个: 1是做业分片, 2是弹性扩容缩容

1. 做业分片就是上面所说的, 把一个大的任务拆分红多个子任务, 而后由多个做业节点去处理这些子任务, 以此缩短做业的时间.
2. 弹性扩容缩容实际上是跟做业分片息息相关的, 简单的理解就是增长或减小一个做业节点, 都能保证每个分片都有节点处理, 每一个节点都有分片可处理.
复制代码

更多elastic-job的知识和原理请参考官网, 相信我再怎么总结也没有官网总结得清晰和完善了.

image

xxl-job

xxl-job是被普遍使用的另一款使用的分布式任务调度框架. 早起的xxljob也是基于quartz开发的, 不过如今慢慢去quartz化了, 改为自研的调度模块.

相对于elastic-job, 我更加喜欢使用xxl-job, 其优势以下:

1. 功能更强大. elastic-job支持的功能, xxl-job基本都支持. 原本我想截一下图的, 结果发现一屏根本截不过来. 你们仍是去官网本身看一下吧.
2. 真正实现调度和执行分离, 相对而言, elastic-job的调度和执行其实糅杂在一块儿的,都是嵌入到业务系统中, 这一点我就不太喜欢了
3. xxl-job的管理后台更加丰富和灵活, 还有我最喜欢的一个点, 就是能够在控制台里面看到任务执行的日志.
复制代码

一样, 因为官方的文档很是详细, 因此我这里再怎么介绍也比不过官网的. 因此更多的特性和原理, 你们能够移步官网

image

总结

本文一共从简单到复杂, 一共介绍了6种调度任务的处理的方案. 固然生产环境中通常都是建议使用elastic-job和xxl-job. 可是若是是简单的任务的话, 使用简单crontab等也不是不可, 我以前就常用crontab作业务相关的定时任务.

固然, 在数据量愈来愈大, 大数据技术发展得也愈来愈快的今天, 像Hadoop,Spark等生态中也出现了很多优秀的定时调度框架.但那就不在本文中的讨论范畴中了.

相关文章
相关标签/搜索