Java-定时任务

java中执行定时任务

定时任务的场景

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

crontab

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

但是由于crontab在实际开发中应用比较多, 而且crontab表达式跟我们后面介绍的其他定时任务框架的cron表达式是类似的, 所以这里还是最先介绍crontab

crontab的用法是:

crontab Expression command

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

# 每小时的第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列表示是星期

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接口没有半毛钱关系(果然够间接的), 完全是自己独立搞了一套, 基本所有调度相关的逻辑都在里面实现了

另外从这个继承图中的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的实现类, 乍一看还挺多. 但是实际就图中红圈圈出来的那几个是真正的实现类, 其他的都是接口或实现类:

而实际上, 我们用得最多的也只是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"。

当然, 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. 弹性扩容缩容其实是跟作业分片息息相关的, 简单的理解就是增加或减少一个作业节点, 都能保证每一个分片都有节点处理, 每个节点都有分片可处理.
xxl-job

xxl-job是被广泛使用的另外一款使用的分布式任务调度框架. 早起的xxljob也是基于quartz开发的, 不过现在慢慢去quartz化了, 改成自研的调度模块.

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

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

总结

本文一共从简单到复杂, 一共介绍了5种调度任务的处理的方案. 当然生产环境中一般都是建议使用elastic-job和xxl-job. 但是如果是简单的任务的话, 使用简单crontab等也不是不可, 我之前就经常使用crontab做业务相关的定时任务.

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

posted @ 2021-06-23 23:42  蔚蓝的海洋  阅读(2445)  评论(0编辑  收藏  举报