HOME/Articles/

定时任务

Article Outline

在JAVA开发过程中经常会遇到使用定时任务的情况,现有的实现方式有以下几种:
1. Timer
2. ScheduledExecutorService
3. SpringTask
4. Quartz
5. XXL-JOB

crontab

要执行定时任务就需要对crontab有一定的了解, linux中的crontab命令定期检查是否有要执行的任务,若检测到便自动执行该任务,从而达到实现定时任务的目的

  • 使用语法

      # -u user指定用户
      crontab [ -u user ] file 
    
      # -l 列出目前的时程表 | -r 删除目前的时程表 | -e 通过vi文字编辑器设定
      crontab [ -u user] { -l | -r | -e }
  • 时间格式

      # f1表示分钟,f2表示小时,f3表示一个月份中的第几日,f4表示月份,f5表示一个星期中的第几天,program表示要执行的程序
      f1 f2 f3 f4 f5 program
    
      # 特别地,指定位置为*表示每个该单位时间都要执行一次,为a-b表示a-b时间段都要执行一次,*/n表示每n个单位时间间隔执行一次
      0 7-12/3 * 12 * /bin/ls
    
      # 上述例子即12月内每日7点~12点每隔3个小时执行一次/bin/ls

Timer

Timer类位于java.util包下,可用于实现定时任务

/* 
    1000ms是延迟启动时间,2000ms是定时任务周期,即每2s执行一次
*/
new Timer("testTimer").schedule(new TimeTask() {
    @Override
    public void run() {
        System.out.println("TimerTask");
    }
}, 1000, 2000);
/*  
    timer有两种方法:schedule(任务结束时开始计算时间间隔)
                   scheduleAtFixedRat(任务开始时开始计算时间间隔)

    date即起始时间,2000ms是定时任务周期,即从起始时间开始每2s执行一次
*/
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
try {
    Date date = dateFormat.parse("2020-01-19 12:00:00.000");
    new Timer("testTimer").scheduleAtFixedRate(new TimerTask () {
        @Override
        public void run() {
            System.out.println("TimerTask");
        }
    }, date, 2000);
} catch(ParseException e) {
    e.printStackTrace();
}

ScheduledExecutorService

ScheduledExecutorService在ExecutorService提供的功能上增加了延迟和定期执行任务的功能。因此,该方法可用于实现定时任务

/*
    延迟1s启动,执行一次
*/
scheduledExecutorService.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("ScheduledTask");
    }
}, 1, TimeUnit.SECONDS);
/*
    延迟1s启动,每隔1s执行一次
*/
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExectorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("ScheduledTask");
    }
}, 1, 1, TimeUnit.SECONDS);
/*
    延迟1s启动,在前一个任务执行完成后,延迟1s再执行
*/
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
Executors.newScheduledThreadPool(10);
scheduledExectorService.scheduleWithFixedDelay(new Runnable() {
    @Override
    public void run() {
        System.out.println("ScheduledTask");
    }
}, 1, 1, TimeUnit.SECONDS);

SpringTask

Spring框架同样提供了实现定时任务的方式,可以看做一个轻量级的quartz框架。该实现分为2部分:
1. 任务类
2. 配置文件

  1. 定时任务类

    @Service
    public class SpringTask {
     private static final Logger log = LoggerFactory.getLogger(SpringTask.class);
    
     /*
         cron格式: {秒} {分} {时} {日期} {月} {星期}
         每隔5s执行一次
     */
     @Scheduled(cron = "1/5 * * * * *")
     public void task1() {
         log.info("SpringTask 定时任务");
     }
    
     /*
         延迟1s,每隔1s执行一次
     */
     @Scheduled(initialDelay = 1000, fixedRate = 1*1000)
     public void task1() {
         log.info("SpringTask 定时任务");
     }
    }
    

2. 配置文件  
```xml
< !-- 简易版 有且只有一个定时任务可用 -->
< task:annotation-driven />

< !-- 任务池版 有多个定时任务则用任务池,否则定时任务会顺序执行 -->
< task:executor id="executor" pool-size="10" />
< task:scheduler id="scheduler" pool-size="10" />
< task:annotation-driven executor="executor" scheduler="scheduler" />

Quartz

本篇主要介绍定时任务的不同实现方式,因此对于Quartz原理只做简要介绍

  • Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制
  • Quartz 可以与 J2EE 与 J2SE 应用程序相结合也可以单独使用
  • Quartz 允许程序开发人员根据时间的间隔来调度作业
  • Quartz 实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联

Quartz核心概念

Quartz.png

  • Scheduler:调度容器
  • Job:Job接口类,即被调度的任务
  • JobDetail :Job的描述类,job执行时的依据此对象的信息反射实例化出Job的具体执行对象
  • Trigger:触发器,存放Job执行的时间策略。用于定义任务调度时间规则
  • JobStore: 存储作业和调度期间的状态
  • Calendar:指定排除的时间点(如排除法定节假日)

Quartz使用

  1. Maven依赖
<!-- quartz -->
<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.3.0</version>
</dependency>
<!--调度器核心包-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>4.3.4.RELEASE</version>
</dependency>
  1. Job实现
public class HelloWorldJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        String strTime = new SimpleDateFormat("HH-mm-ss").format(new Date());
        System.out.println(strTime + ":Hello World! ");
    }
}
  1. 调度器(可用listner在项目启动时执行)
public MyScheduler {
    public static void main(String[] args) throws SchedulerException {
        // 创建调度器
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Sccheduler scheduler = schedulerFactory.getScheduler();

        // 创建JobDetail实例
        JobDetail jobDetail = JobBuilder.newJob(HelloWorldJob.class).withIdentity("job1", "jobGroup1").build();

        // 创建触发器Trigger实例
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "triggerGroup1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever)
                .build();

        // 开始执行
        scheduler.scheduleJob(jobDetail, trigger);
        scheduler.start();        
    }
}
/*
    上例是简单触发器,如下是crontab触发器
*/
Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger2", "triggerGroup2")
        .startNow()
        .withSchedule(cronScheduler("0 42 10 * * ?"))
        .build();
  1. 整合Spring 将上方的调度器改写成配置文件即可整合到Spring中

XXL-JOB

如果定时任务维护在每个微服务下,而微服务部署多个实例的情况下,会出现定时任务多次执行的情况。此外定时任务有时希望能实现动态修改任务的定时时间。
XXL-JOB即可满足该需求,它是一个轻量级分布式任务调度平台,内容采用Quartz定时框架实现,服务间通信通过RPC方式实现。
功能方面可参考官方文档

服务端实现过程

  1. XXL-JOB源码下载
    github地址
    xxl-job-core是核心代码模块,xxl-job-admin为任务调度中心管理模块。仅部署可仅使用xxl-job-admin

  2. 数据库创建
    sql文件为:xxl-job/doc/db/tables_xxl_job.sql
    在mysql数据库中执行该sql,能生成新的数据库xxl-job及相关数据库表xxl_job_* 目前仅支持mysql数据库,需支持其他数据库可修改其源码实现

  3. xxl-job-admin模块配置修改 修改xxl-job-admin下的配置文件:application.properties
    主要需修改以下几处:

    • 数据库地址 (spring.datasource.url)
    • 用户名 (spring.datasource.username)
    • 密码 (spring.datasource.password)
    • 端口号(server.port)默认为8080,可修改
    • 访问地址(server.context-path)默认为xxl-job-admin,可修改
  4. 启动xxl-job-admin微服务 本地启动成功后,可通过访问本地地址来访问任务调度中心
    用户名密码默认为: admin/123456

客户端实现过程

  1. Maven依赖
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.0.2</version>
</dependency>
  1. 修改配置文件
    ip可不填,会自动识别注册
xxl:
  job:
    admin:
      addresses: http://localhost:8080/xxl-job-admin
    executor:
      ip:
      port: 9999
      logpath: /data/applogs/xxl-job/jobhandler
      appname: plat-job
    accessToken:     
  1. 编写Configuration配置项类 需保证服务端与客户端版本一致
@Configuration
public class XxlJobConfig {

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appName;

    @Value("${xxl.job.executor.ip}")
    private String ip;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    private int logRetentionDays = -1;

    @Bean(initMethod = "start", destoryMethod = "destory")
    public XxlJobSpringExecutor xxlJobExecutor() {
        System.out.println("xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppName(appName);
        xxlJobSpringExecutorsetIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    } 
}
  1. 编写Handler类(实际处理任务) Handler在下例中命名为"TestHandler",需保证唯一
@JobHandler(value = "TestHandler")
@Component
public class TestHandler extends IJobHandler {
    @Override
    public ReturnT<String> execute(String param) throws Exception {
        System.out.println("执行了TestHandler一次");
        return SUCCESS;
    }
}
  1. 启动客户端 启动成功后,服务器中会自动增加一个名为plat-job的执行器,用来执行定时任务

服务端页面配置

  1. 修改执行器 xxl-job-executor.png 对执行器进行编辑,注册方式改为【手动录入】,机器地址写入客户端(微服务)具体的ip:port后,点击保存

  2. 任务管理编辑 xxl-job-edit.png 通过如图方式即可动态更新定时计划

总结

由于每个微服务的ip和port可以在执行器的机器地址中,通过逗号分隔的方式,可增加多个客户端的地址,则解决了项目中多实例的问题。因此XXL-JOB满足了定时任务的额外需求