在线corn表达式

1. 总结常见的实现定时任务的几种方法

  • thread实现 【原理:通过创建一个线程,让他在while循环里面一直运行,用sleep() 方法让其休眠从而达到定时任务的效果。】
  • Timer类
  • ScheduledExcecutorService类
  • 使用springboot的 spring-task 实现
  • Quartz
  • xxl-job



以下演示几种实现方式:每隔一秒打印一次hello world

1.1 thread实现

public static void main(String[] args) {
	final long timeInterval = 1000;
	
	Runnable runnable = new Runnable() {
		@Override
		public void run() {
			while (true) {
				System.out.println("hello world");
				try {
					Thread.sleep(timeInterval);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	};
	
	Thread thread = new Thread(runnable);
	thread.start();
}




1.2 Timer类实现

Timer是jdk中自带的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。但封装任务的类是TimerTask类(实际该类是一个抽象类,执行任务的代码要放在该类的子类中)。

TimerTask是一个实现了Runnable接口的抽象类,代表一个可以被Timer执行的任务。

构造方法:

定时任务架构设计 定时任务实现方式_定时任务


成员方法:

定时任务架构设计 定时任务实现方式_定时任务架构设计_02

public static void main(String[] args) {
	TimerTask task = new TimerTask() {
		@Override
		public void run() {
			System.out.println("Hello!Word!");
		}
	};
	
	Timer timer = new Timer();
	long delay = 0;
	long intevalPeriod = 1 * 1000;
	timer.scheduleAtFixedRate(task, delay, intevalPeriod);
}

schedule 与 scheduleAtFixedRate区别:

schedule会保证任务的间隔是按照定义的period参数严格执行的,如果某一次调度时间比较长,那么后面的时间会顺延,保证调度间隔都是period。
scheduleAtFixedRate是严格按照调度时间来的,如果某次调度时间太长了,那么会通过缩短间隔的方式保证下一次调度在预定时间执行。

线程安全, 但只会单线程执行, 如果执行时间过长, 就错过下次任务了, 抛出异常时, timerWork会终止
启动和取消任务是可以控制的



1.3 ScheduledExcecutorService类实现

ScheduledExecutorService是JDK1.5以后java.util.concurrent中的一个接口, 用于实现定时任务。

public static void main(String[] args) {
	Runnable r1 = new Runnable() {
		public void run() {
			System.out.println("Hello Word");
		}
	};
	ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
	// 第二个参数为首次执行的延时时间,第三个参数为定时执行的间隔时间
	service.scheduleAtFixedRate(r1, 3, 1, TimeUnit.SECONDS);// 3秒后开始执行
}

他是通过线程池的方式执行任务,可以多线程执行。
启动和取消任务是可以控制的
可以设定第一次的延迟时间



1.4 使用springboot提供的 spring-task 实现【这玩意其实是Spring里面的,不过现在都是SpringBoot开发,所以都是基于注解来使用它】

a) 只需要导入web的starter依赖
b) 在启动类上添加 @EnableScheduling 注解
c) 用 @Scheduled 注解开启一个定时任务。

@Component
public class SchedulerTask {
	private int count = 0;
	
	/**
	 * @Author Smith
	 * @Description 设置每1秒执行一次
	 * @Date 14:23 2019/1/24
	 * @Param
	 * @return void
	 **/
	@Scheduled(cron = "*/1 * * * * ?") // 基于cron表达式实现
	private void process(){
		System.out.println("hello world " + (count++));
	}
}

@Scheduled 该注解的常用参数说明【不基于cron表达式实现的时候】:
fixedRate 表示任务执行之间的时间间隔,具体是指两次任务的开始时间间隔,即第二次任务开始时,第一次任务可能还没结束。
fixedDelay 表示任务执行之间的时间间隔,具体是指本次任务结束到下次任务开始之间的时间间隔。
initialDelay 表示首次任务启动的延迟时间。
所有时间的单位都是毫秒。



1.5 Quartz

参考源码 Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。

Quartz默认是多线程异步执行,单个任务时,在上一个调度未完成时,下一个调度时间到时,会另起一个线程开始新的调度。
Quartz支持集群定时任务

Quartz与Spring Task区别

  • Quartz默认多线程异步执行,Task默认单线程同步执行。
  • Quartz单个任务时,在上一个调度未完成时,下一个调度时间到时,会另起一个线程开始新的调度。Task单个任务时,当前次的调度完成后,再执行下一次任务调度。
  • Quartz多个任务时,任务之间没有直接影响,多任务执行的快慢取决于CPU的性能。Task多个任务时,一个任务执行完成后才会执行下一个任务。若需要任务能够并发执行,需手动设置线程池
  • Quartz可以采用集群方式,分布式部署到多台机器,分配执行定时任务

两者对比总结:
1、实现,Task注解实现方式,比较简单。Quartz需要手动配置Jobs。
2、任务执行,Task默认单线程串行执行任务,多任务时若某个任务执行时间过长,后续任务会无法及时执行。Quartz采用多线程,无这个问题。
3、调度,Task采用顺序执行,若当前调度占用时间过长,下一个调度无法及时执行;
4、Quartz采用异步,下一个调度时间到达时,会另一个线程执行调度,不会发生阻塞问题,但调度过多时可能导致数据处理异常
5、部署,Quartz可以采用集群方式,分布式部署到多台机器,分配执行定时任务


Quartz 核心概念:
a) Job 表示一个工作,要执行的具体内容。此接口中只有一个方法。Task多个任务时,一个任务执行完成后才会执行下一个任务。若需要任务能够并发执行,需手动设置线程池。

void execute(JobExecutionContext context)

b) JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
c) Trigger 代表一个调度参数的配置,什么时候去调。
d) Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。

Spring Boot整合Quartz:

  1. 导入Quartz依赖
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>


  1. 编写任务类
public class Task implements Job {

        // 任务类不能直接注入Bean,若想注入Bean需要配置第4步
	@Autowired 
	private TestService service;

	private void before(){
		System.out.println("定时任务开始");
	}

	@Override
	public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
		service.testDemo();
		System.out.println("hello quartz");
	}
	
	private void afer(){
		System.out.println("定时任务结束");
	}
}
★ 任务类不能直接注入Bean,若想注入Bean需要配置第4步 ★


  1. 编写Quartz配置
@Configuration
public class QuartzConfig {
	
	@Bean
	public JobDetailFactoryBean jobDetailFactoryBean() {
		JobDetailFactoryBean factory = new JobDetailFactoryBean();
		//关联我们自己的Job类
		factory.setJobClass(Task.class);
		return factory;
	}
	
	@Bean
	public CronTriggerFactoryBean cronTriggerFactoryBean(JobDetailFactoryBean jobDetailFactoryBean) {
		CronTriggerFactoryBean factory = new CronTriggerFactoryBean();
		factory.setJobDetail(jobDetailFactoryBean.getObject());
		//设置触发时间
		factory.setCronExpression("0/2 * * * * ?");
		return factory;
	}
	
	@Bean
	public SchedulerFactoryBean schedulerFactoryBean(CronTriggerFactoryBean cronTriggerFactoryBean, MyAdaptableJobFactory jobFactory) {
		SchedulerFactoryBean factory = new SchedulerFactoryBean();
		//关联trigger
		factory.setTriggers(cronTriggerFactoryBean.getObject());
		factory.setJobFactory(jobFactory);
		return factory;
	}
}


  1. 编写配置【用于任务类能注入我们的bean】
@Component
public class MyAdaptableJobFactory extends AdaptableJobFactory {

    // 可以将一个对象添加到SpringIOC 容器中,并且完成该对象注入
    @Autowired
    private AutowireCapableBeanFactory autowireCapableBeanFactory;

    /**
     * 该方法需要将实例化的任务对象手动的添加到springIOC 容器中并且完成对象的注入
     */
    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        Object obj = super.createJobInstance(bundle);
        //将obj 对象添加Spring IOC 容器中,并完成注入
        this.autowireCapableBeanFactory.autowireBean(obj);
        return obj;
    }
}



补充:分布式任务调度

如果在分布式集群的模式下,采用集中式的任务调度方式,会带来一些问题。如:

  1. 多台机器集群部署时,该机器的定时任务如何保证不被重复执行?
  2. 如何动态的调整定时任务的执行时间(不重启服务器的情况)?
  3. 若某一台部署定时任务的机器发生故障时,如何转移故障给其他机器?
  4. 如何对定时任务进行监控?
  5. 业务量大,单机性能出现瓶颈问题,如何实现扩展

补充:分布式任务调度解决方案

  1. 数据库唯一约束,避免定时任务重复执行
  2. 使用分布式锁实现定时任务的调度控制
  3. 使用配置文件,redis,mysql,全局属性等作为调度的开关
  4. 使用分布式任务调度平台:XXL-JOB,Elastic-Job...

2 Quartz[群集环境]

使用quartz实现定时任务[单机版],若是部署多台机器,那么到了时间点,多台服务器便会同时均开始执行定时任务。
Quartz是能适用于分布式集群环境的,在同一时间只会有一台机器执行定时任务。

Quartz 中集群如何工作:
一个 Quartz 集群中的每个节点是一个独立的 Quartz 应用,它又管理着其他的节点。意思是你必须对每个节点分别启动或停止。不像许多应用服务器的集群,独立的 Quartz 节点并不与另一其的节点或是管理节点通信。Quartz 应用是通过数据库表来感知到另一应用的。离开了db将无法感知

2.1 导入依赖

<!-- spring boot2.x + quartz -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>


2.2 数据库建表

到官网下载

下载之后解压,进入如下目录,创建数据库表:

定时任务架构设计 定时任务实现方式_定时任务_03




定时任务架构设计 定时任务实现方式_spring_04




11张表功能说明:

定时任务架构设计 定时任务实现方式_任务调度_05




3 Xxl-Job入门详解

官网文档地址:https://www.xuxueli.com/xxl-job/ 源码:http://gitee.com/xuxueli0323/xxl-job

3.1 基础概念

定时任务架构设计 定时任务实现方式_定时任务架构设计_06

调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。

任务执行器:负责接收调度请求并执行任务逻辑。

任务:专注于任务的处理。

调度中心会发出调度请求,任务执行器接收到请求之后会去执行任务,任务则专注于任务业务的处理。


3.2 搭建调度中心

  1. 创建数据表【请下载项目源码并解压,获取 “调度数据库初始化SQL脚本” 并执行即可。】
  • xxl_job_lock:任务调度锁表;
  • xxl_job_group:执行器信息表,维护任务执行器信息;
  • xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
  • xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
  • xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
  • xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
  • xxl_job_user:系统用户表;


  1. 下载调度中心源码



  1. 配置部署调度中心
    调度中心项目:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
    调度中心配置文件地址:/xxl-job/xxl-job-admin/src/main/resources/application.properties
### 数据库的连接信息修改为自己的数据库
    ### web
    server.port=8080
    server.servlet.context-path=/xxl-job-admin

    ### actuator
    management.server.servlet.context-path=/actuator
    management.health.mail.enabled=false

    ### resources
    spring.mvc.servlet.load-on-startup=0
    spring.mvc.static-path-pattern=/static/**
    spring.resources.static-locations=classpath:/static/

    ### freemarker
    spring.freemarker.templateLoaderPath=classpath:/templates/
    spring.freemarker.suffix=.ftl
    spring.freemarker.charset=UTF-8
    spring.freemarker.request-context-attribute=request
    spring.freemarker.settings.number_format=0.##########

    ### mybatis
    mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
    #mybatis.type-aliases-package=com.xxl.job.admin.core.model

    ### xxl-job, datasource
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?Unicode=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

    ### datasource-pool
    spring.datasource.type=com.zaxxer.hikari.HikariDataSource
    spring.datasource.hikari.minimum-idle=10
    spring.datasource.hikari.maximum-pool-size=30
    spring.datasource.hikari.auto-commit=true
    spring.datasource.hikari.idle-timeout=30000
    spring.datasource.hikari.pool-name=HikariCP
    spring.datasource.hikari.max-lifetime=900000
    spring.datasource.hikari.connection-timeout=10000
    spring.datasource.hikari.connection-test-query=SELECT 1

    ### xxl-job, email
    spring.mail.host=smtp.qq.com
    spring.mail.port=25
    spring.mail.username=xxx@qq.com
    spring.mail.password=xxx
    spring.mail.properties.mail.smtp.auth=true
    spring.mail.properties.mail.smtp.starttls.enable=true
    spring.mail.properties.mail.smtp.starttls.required=true
    spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

    ### xxl-job, access token
    xxl.job.accessToken=

    ### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")
    xxl.job.i18n=zh_CN

    ## xxl-job, triggerpool max size
    xxl.job.triggerpool.fast.max=200
    xxl.job.triggerpool.slow.max=100

    ### xxl-job, log retention days
    xxl.job.logretentiondays=30
  1. 部署项目
    可将项目编译打包部署,或者在idea里面启动调度中心。
  2. 访问调度中心
    调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址)
    默认登录账号 “admin/123456”, 登录后运行界面如下图所示。

  3. 配置调度中心的执行器

    此处的AppName,会在创建任务时被选择,每个任务必然要选择一个执行器。





    6.1 新增执行器:



    自动注册和手动注册的区别和配置:

    自动注册:执行器自动进行执行器注册,调度中心通过底层注册表可以动态发现执行器机器地址;
    手动录入:人工手动录入执行器的地址信息,多地址逗号分隔,供调度中心使用;


    6.2 在调度中心新建任务



    6.2.1 执行器:
    任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能; 另一方面也可以方便的进行任务分组。
    每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置。


    6.2.2 路由策略:当执行器集群部署时,提供丰富的路由策略,包括



    6.2.3 运行模式:
  • BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;
  • GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务;
  • GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本;
  • GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本;
  • GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本;
  • GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本;
  • GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本;


    6.2.4 阻塞处理策略:
  • 单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;
  • 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
  • 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;


    6.2.5 JobHandler:
    运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值;


    6.2.6 子任务:
    每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。
  1. 与SpringBoot整合
    如果项目中没有找到xxl-job-core这个依赖,需要把这个依赖安装到本地的maven仓库。
<groupId>com.itheima</groupId>
<artifactId>xxl-job-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 继承Spring boot工程 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- xxl-job -->
    <dependency>
        <groupId>com.xuxueli</groupId>
        <artifactId>xxl-job-core</artifactId>
        <version>2.2.0-SNAPSHOT</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>


7.1 创建配置文件

### web port
server.port=${port:8801}
### no web
#spring.main.web-environment=false

### log config
logging.config=classpath:logback.xml


### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://localhost:8888/xxl-job-admin

### xxl-job, access token
xxl.job.accessToken=

### xxl-job executor appname   改成你XXL-JOB调度中心的执行器 AppName
xxl.job.executor.appname=xxl-job-executor-sample
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
xxl.job.executor.port=${executor.port:9999}
### xxl-job executor log-path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30


7.2 添加xxl-job配置 添加配置类:这个类主要是创建了任务执行器,参考官方案例编写,无须改动。

package com.itheima.xxljob.config;

import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

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

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

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

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

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

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

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

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


    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppName(appName);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

    /**
     * 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
     *
     *      1、引入依赖:
     *          <dependency>
     *             <groupId>org.springframework.cloud</groupId>
     *             <artifactId>spring-cloud-commons</artifactId>
     *             <version>${version}</version>
     *         </dependency>
     *
     *      2、配置文件,或者容器启动变量
     *          spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
     *
     *      3、获取IP
     *          String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
     */


}


7.3 创建任务

@Component
public class HelloJob {

    @Value("${server.port}")
    private String appPort;

    @XxlJob("helloJob")
    public ReturnT<String> hello(String param) throws Exception {
        System.out.println("helloJob:"+ LocalDateTime.now()+",端口号"+appPort);
        return ReturnT.SUCCESS;
    }
}

@XxlJob("helloJob")这个一定要与调度中心新建任务的JobHandler的值保持一致,如下图:

定时任务架构设计 定时任务实现方式_定时任务架构设计_07




7.4 测试

(1)首先启动调度中心

(2)启动xxl-job-demo项目,为了展示更好的效果,可以同时启动三个项目,用同一个JobHandler,查看处理方式。

在启动多个项目的时候,端口需要切换,连接xxl-job的执行器端口不同相同

服务一:默认启动8801端口,执行器端口为9999

idea中不用其他配置,直接启动项目即可

服务二:项目端口:8802,执行器端口:9998

idea配置如下:

  • 编辑配置,Edit Configurations...