你们好,我是艿艿,一个平常在地铁撸码的小胖子~html
今天咱们来一块儿瞅瞅,在 Spring Boot 应用中,有哪些定时任务的技术选型~java
在产品的色彩斑斓的黑的需求中,有存在一类需求,是须要去定时执行的,此时就须要使用到定时任务。例如说,每分钟扫描超时支付的订单,每小时清理一第二天志文件,天天统计前一天的数据并生成报表,每月月初的工资单的推送,每一年一次的生日提醒等等。mysql
其中,艿艿最喜欢“每月月初的工资单的推送”,你呢?
在 JDK 中,内置了两个类,能够实现定时任务的功能:git
java.util.Timer
:能够经过建立 java.util.TimerTask
调度任务,在同一个线程中串行执行,相互影响。也就是说,对于同一个 Timer 里的多个 TimerTask 任务,若是一个 TimerTask 任务在执行中,其它 TimerTask 即便到达执行的时间,也只能排队等待。由于 Timer 是串行的,同时存在 坑坑 ,因此后来 JDK 又推出了 ScheduledExecutorService ,Timer 也基本再也不使用。java.util.concurrent.ScheduledExecutorService
:在 JDK 1.5 新增,基于线程池设计的定时任务类,每一个调度任务都会被分配到线程池中并发执行,互不影响。这样,ScheduledExecutorService 就解决了 Timer 串行的问题。在平常开发中,咱们不多直接使用 Timer 或 ScheduledExecutorService 来实现定时任务的需求。主要有几点缘由:github
因此,通常状况下,咱们会选择专业的调度任务中间件。web
关于“ 任务”的叫法,也有叫“ 做业”的。在英文上,有 Task 也有 Job 。本质是同样的,本文两种都会用。而后,通常来讲是调度任务,定时执行。因此胖友会在本文,或者其它文章中,会看到“调度”或“定时”的字眼儿。面试
在 Spring 体系中,内置了两种定时任务的解决方案:spring
第二种,Spring Boot 2.0 版本,整合了 Quartz 做业调度框架,提供了功能强大的定时任务的实现。sql
注:Spring Framework 已经内置了 Quartz 的整合。Spring Boot 1.X 版本未提供 Quartz 的自动化配置,而 2.X 版本提供了支持。
在 Java 生态中,还有很是多优秀的开源的调度任务中间件:数据库
惟品会基于 Elastic-Job 之上,演化出了 Saturn 项目。
目前国内采用 Elastic-Job 和 XXL-JOB 为主。从艿艿了解到的状况,使用 XXL-JOB 的团队会更多一些,主要是上手较为容易,运维功能更为完善。
本文在提供完整代码示例,可见 https://github.com/YunaiV/Spr... 的 lab-28 目录。原创不易,给点个 Star 嘿,一块儿冲鸭!
示例代码对应仓库: lab-28-task-demo 。
考虑到实际场景下,咱们不多使用 Spring Task ,因此本小节会写的比较简洁。若是对 Spring Task 比较感兴趣的胖友,能够本身去阅读 《Spring Framework Documentation —— Task Execution and Scheduling》 文档,里面有 Spring Task 相关的详细文档。
在本小节,咱们会使用 Spring Task 功能,实现一个每 2 秒打印一行执行日志的定时任务。
在 pom.xml
文件中,引入相关依赖。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>lab-28-task-demo</artifactId> <dependencies> <!-- 实现对 Spring MVC 的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>
由于 Spring Task 是 Spring Framework 的模块,因此在咱们引入 spring-boot-starter-web
依赖后,无需特别引入它。
同时,考虑到咱们但愿让项目启动时,不自动结束 JVM 进程,因此咱们引入了 spring-boot-starter-web
依赖。
在 cn.iocoder.springboot.lab28.task.config
包路径下,建立 ScheduleConfiguration 类,配置 Spring Task 。代码以下:
// ScheduleConfiguration.java @Configuration @EnableScheduling public class ScheduleConfiguration { }
@EnableScheduling
注解,启动 Spring Task 的定时任务调度的功能。在 cn.iocoder.springboot.lab28.task.job
包路径下,建立 DemoJob 类,示例定时任务类。代码以下:
// DemoJob.java @Component public class DemoJob { private Logger logger = LoggerFactory.getLogger(getClass()); private final AtomicInteger counts = new AtomicInteger(); @Scheduled(fixedRate = 2000) public void execute() { logger.info("[execute][定时第 ({}) 次执行]", counts.incrementAndGet()); } }
@Component
注解,建立 DemoJob Bean 对象。#execute()
方法,实现打印日志。同时,在该方法上,添加 @Scheduled
注解,设置每 2 秒执行该方法。虽说,@Scheduled
注解,能够添加在一个类上的多个方法上,可是艿艿的我的习惯上,仍是一个 Job 类,一个定时任务。😈
建立 Application.java
类,配置 @SpringBootApplication
注解便可。代码以下:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
运行 Application 类,启动示例项目。输出日志精简以下:
# 初始化一个 ThreadPoolTaskScheduler 任务调度器 2019-11-30 18:02:58.415 INFO 83730 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler' # 每 2 秒,执行一次 DemoJob 的任务 2019-11-30 18:02:58.449 INFO 83730 --- [ pikaqiu-demo-1] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (1) 次执行] 2019-11-30 18:03:00.438 INFO 83730 --- [ pikaqiu-demo-1] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (2) 次执行] 2019-11-30 18:03:02.442 INFO 83730 --- [ pikaqiu-demo-2] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (3) 次执行]
至此,咱们已经完成了 Spring Task 调度任务功能的入门。实际上,Spring Task 还提供了异步任务 ,这个咱们在其它文章中,详细讲解。
下面 「2.5 @Scheduled」和 「2.6 应用配置文件」两个小节,是补充知识,建议看看。
@Scheduled
注解,设置定时任务的执行计划。
经常使用属性以下:
cron
属性:Spring Cron 表达式。例如说,"0 0 12 * * ?"
表示天天中午执行一次,"11 11 11 11 11 ?"
表示 11 月 11 号 11 点 11 分 11 秒执行一次(哈哈哈)。更多示例和讲解,能够看看 《Spring Cron 表达式》 文章。注意,以调用完成时刻为开始计时时间。fixedDelay
属性:固定执行间隔,单位:毫秒。注意,以调用完成时刻为开始计时时间。fixedRate
属性:固定执行间隔,单位:毫秒。注意,以调用开始时刻为开始计时时间。不经常使用属性以下:
initialDelay
属性:初始化的定时任务执行延迟,单位:毫秒。zone
属性:解析 Spring Cron 表达式的所属的时区。默认状况下,使用服务器的本地时区。initialDelayString
属性:initialDelay
的字符串形式。fixedDelayString
属性:fixedDelay
的字符串形式。fixedRateString
属性:fixedRate
的字符串形式。在 application.yml
中,添加 Spring Task 定时任务的配置,以下:
spring: task: # Spring Task 调度任务的配置,对应 TaskSchedulingProperties 配置类 scheduling: thread-name-prefix: pikaqiu-demo- # 线程池的线程名的前缀。默认为 scheduling- ,建议根据本身应用来设置 pool: size: 10 # 线程池大小。默认为 1 ,根据本身应用来设置 shutdown: await-termination: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true await-termination-period: 60 # 等待任务完成的最大时长,单位为秒。默认为 0 ,根据本身应用来设置
spring.task.scheduling
配置项,Spring Task 调度任务的配置,对应 TaskSchedulingProperties 配置类。注意,spring.task.scheduling.shutdown
配置项,是为了实现 Spring Task 定时任务的优雅关闭。咱们想象一下,若是定时任务在执行的过程当中,若是应用开始关闭,把定时任务须要使用到的 Spring Bean 进行销毁,例如说数据库链接池,那么此时定时任务还在执行中,一旦须要访问数据库,可能会致使报错。
await-termination = true
,实现应用关闭时,等待定时任务执行完成。这样,应用在关闭的时,Spring 会优先等待 ThreadPoolTaskScheduler 执行完任务以后,再开始 Spring Bean 的销毁。await-termination-period = 60
,等待任务完成的最大时长,单位为秒。具体设置多少的等待时长,能够根据本身应用的须要。示例代码对应仓库: lab-28-task-quartz-memory 。
在艿艿最先开始实习的时候,公司使用 Quartz 做为任务调度中间件。考虑到咱们要实现定时任务的高可用,须要部署多个 JVM 进程。比较舒服的是,Quartz 自带了集群方案。它经过将做业信息存储到关系数据库中,并使用关系数据库的行锁来实现执行做业的竞争,从而保证多个进程下,同一个任务在相同时刻,不能重复执行。
可能不少胖友对 Quartz 还不是很了解,咱们先来看一段简介:
FROM https://www.oschina.net/p/quartzQuartz 是一个开源的做业调度框架,它彻底由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵活性而不牺牲简单性。你可以用它来为执行一个做业而建立简单的或复杂的调度。
它有不少特征,如:数据库支持,集群,插件,EJB 做业预构建,JavaMail 及其它,支持 cron-like 表达式等等。
在 Quartz 体系结构中,有三个组件很是重要:
不了解的胖友,能够直接看看 《Quartz 入门详解》 文章。这里,艿艿就不重复赘述。
FROM https://medium.com/@ChamithKo...
Quartz 分红单机模式和集群模式。
😈 下面,让咱们开始遨游~
在 pom.xml
文件中,引入相关依赖。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>lab-28-task-quartz-memory</artifactId> <dependencies> <!-- 实现对 Spring MVC 的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 实现对 Quartz 的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> </dependencies> </project>
具体每一个依赖的做用,胖友本身认真看下艿艿添加的全部注释噢。
在 cn.iocoder.springboot.lab28.task.config.job
包路径下,咱们来建立示例 Job 。
建立 DemoJob01 类,示例定时任务 01 类。代码以下:
// DemoJob01.java public class DemoJob01 extends QuartzJobBean { private Logger logger = LoggerFactory.getLogger(getClass()); private final AtomicInteger counts = new AtomicInteger(); @Autowired private DemoService demoService; @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { logger.info("[executeInternal][定时第 ({}) 次执行, demoService 为 ({})]", counts.incrementAndGet(), demoService); } }
#executeInternal(JobExecutionContext context)
方法,执行自定义的定时任务的逻辑。QuartzJobBean 实现了 org.quartz.Job
接口,提供了 Quartz 每次建立 Job 执行定时逻辑时,将该 Job Bean 的依赖属性注入。例如说,DemoJob01 须要 @Autowired
注入的 demoService
属性。核心代码以下:
// QuartzJobBean.java public final void execute(JobExecutionContext context) throws JobExecutionException { try { // 将当前对象,包装成 BeanWrapper 对象 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); // 设置属性到 bw 中 MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValues(context.getScheduler().getContext()); pvs.addPropertyValues(context.getMergedJobDataMap()); bw.setPropertyValues(pvs, true); } catch (SchedulerException ex) { throw new JobExecutionException(ex); } // 执行提供给子类实现的抽象方法 this.executeInternal(context); } protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
counts
属性,计数器。用于咱们后面咱们展现,每次 DemoJob01 都会被 Quartz 建立出一个新的 Job 对象,执行任务。这个很重要,也要很是当心。建立 DemoJob02 类,示例定时任务 02 类。代码以下:
// DemoJob02.java public class DemoJob02 extends QuartzJobBean { private Logger logger = LoggerFactory.getLogger(getClass()); @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { logger.info("[executeInternal][我开始的执行了]"); } }
在 cn.iocoder.springboot.lab28.task.config
包路径下,建立 ScheduleConfiguration 类,配置上述的两个示例 Job 。代码以下:
// ScheduleConfiguration.java @Configuration public class ScheduleConfiguration { public static class DemoJob01Configuration { @Bean public JobDetail demoJob01() { return JobBuilder.newJob(DemoJob01.class) .withIdentity("demoJob01") // 名字为 demoJob01 .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。由于建立 JobDetail 时,还没 Trigger 指向它,因此须要设置为 true ,表示保留。 .build(); } @Bean public Trigger demoJob01Trigger() { // 简单的调度计划的构造器 SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(5) // 频率。 .repeatForever(); // 次数。 // Trigger 构造器 return TriggerBuilder.newTrigger() .forJob(demoJob01()) // 对应 Job 为 demoJob01 .withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder .build(); } } public static class DemoJob02Configuration { @Bean public JobDetail demoJob02() { return JobBuilder.newJob(DemoJob02.class) .withIdentity("demoJob02") // 名字为 demoJob02 .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。由于建立 JobDetail 时,还没 Trigger 指向它,因此须要设置为 true ,表示保留。 .build(); } @Bean public Trigger demoJob02Trigger() { // 基于 Quartz Cron 表达式的调度计划的构造器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *"); // Trigger 构造器 return TriggerBuilder.newTrigger() .forJob(demoJob02()) // 对应 Job 为 demoJob02 .withIdentity("demoJob02Trigger") // 名字为 demoJob02Trigger .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder .build(); } } }
#demoJob01()
方法,建立 DemoJob01 的 JobDetail Bean 对象。#demoJob01Trigger()
方法,建立 DemoJob01 的 Trigger Bean 对象。其中,咱们使用 SimpleScheduleBuilder 简单的调度计划的构造器,建立了每 5 秒执行一次,无限重复的调度计划。#demoJob2()
方法,建立 DemoJob02 的 JobDetail Bean 对象。#demoJob02Trigger()
方法,建立 DemoJob02 的 Trigger Bean 对象。其中,咱们使用 CronScheduleBuilder 基于 Quartz Cron 表达式的调度计划的构造器,建立了每第 10 秒执行一次的调度计划。这里,推荐一个 Quartz/Cron/Crontab 表达式在线生成工具 ,方便帮咱们生成 Quartz Cron 表达式,并计算出最近 5 次运行时间。😈 由于 JobDetail 和 Trigger 通常是成双成对出现,因此艿艿习惯配置成一个 Configuration 配置类。
建立 Application.java
类,配置 @SpringBootApplication
注解便可。代码以下:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
运行 Application 类,启动示例项目。输出日志精简以下:
# 建立了 Quartz QuartzScheduler 并启动 2019-11-30 23:40:05.123 INFO 92812 --- [ main] org.quartz.impl.StdSchedulerFactory : Using default implementation for ThreadExecutor 2019-11-30 23:40:05.130 INFO 92812 --- [ main] org.quartz.core.SchedulerSignalerImpl : Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 2019-11-30 23:40:05.130 INFO 92812 --- [ main] org.quartz.core.QuartzScheduler : Quartz Scheduler v.2.3.2 created. 2019-11-30 23:40:05.131 INFO 92812 --- [ main] org.quartz.simpl.RAMJobStore : RAMJobStore initialized. 2019-11-30 23:40:05.132 INFO 92812 --- [ main] org.quartz.core.QuartzScheduler : Scheduler meta-data: Quartz Scheduler (v2.3.2) 'quartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 2019-11-30 23:40:05.132 INFO 92812 --- [ main] org.quartz.impl.StdSchedulerFactory : Quartz scheduler 'quartzScheduler' initialized from an externally provided properties instance. 2019-11-30 23:40:05.132 INFO 92812 --- [ main] org.quartz.impl.StdSchedulerFactory : Quartz scheduler version: 2.3.2 2019-11-30 23:40:05.132 INFO 92812 --- [ main] org.quartz.core.QuartzScheduler : JobFactory set to: org.springframework.scheduling.quartz.SpringBeanJobFactory@203dd56b 2019-11-30 23:40:05.158 INFO 92812 --- [ main] o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now 2019-11-30 23:40:05.158 INFO 92812 --- [ main] org.quartz.core.QuartzScheduler : Scheduler quartzScheduler_$_NON_CLUSTERED started. # DemoJob01 2019-11-30 23:40:05.164 INFO 92812 --- [eduler_Worker-1] c.i.springboot.lab28.task.job.DemoJob01 : [executeInternal][定时第 (1) 次执行, demoService 为 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)] 2019-11-30 23:40:09.866 INFO 92812 --- [eduler_Worker-2] c.i.springboot.lab28.task.job.DemoJob01 : [executeInternal][定时第 (1) 次执行, demoService 为 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)] 2019-11-30 23:40:14.865 INFO 92812 --- [eduler_Worker-4] c.i.springboot.lab28.task.job.DemoJob01 : [executeInternal][定时第 (1) 次执行, demoService 为 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)] # DemoJob02 2019-11-30 23:40:10.004 INFO 92812 --- [eduler_Worker-3] c.i.springboot.lab28.task.job.DemoJob02 : [executeInternal][我开始的执行了] 2019-11-30 23:40:20.001 INFO 92812 --- [eduler_Worker-6] c.i.springboot.lab28.task.job.DemoJob02 : [executeInternal][我开始的执行了] 2019-11-30 23:40:30.002 INFO 92812 --- [eduler_Worker-9] c.i.springboot.lab28.task.job.DemoJob02 : [executeInternal][我开始的执行了]
demoService
成功注入,而 counts
每次都是 1 ,说明每次 DemoJob01 都是新建立的。下面 「3.5 应用配置文件」两个小节,是补充知识,建议看看。
在 application.yml
中,添加 Quartz 的配置,以下:
spring: # Quartz 的配置,对应 QuartzProperties 配置类 quartz: job-store-type: memory # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 auto-startup: true # Quartz 是否自动启动 startup-delay: 0 # 延迟 N 秒启动 wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true overwrite-existing-jobs: false # 是否覆盖已有 Job 的配置 properties: # 添加 Quartz Scheduler 附加属性,更多能够看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 org: quartz: threadPool: threadCount: 25 # 线程池大小。默认为 10 。 threadPriority: 5 # 线程优先级 class: org.quartz.simpl.SimpleThreadPool # 线程池类型 # jdbc: # 这里暂时不说明,使用 JDBC 的 JobStore 的时候,才须要配置
spring.quartz
配置项,Quartz 的配置,对应 QuartzProperties 配置类。注意,spring.quartz.wait-for-jobs-to-complete-on-shutdown
配置项,是为了实现 Quartz 的优雅关闭,建议开启。关于这块,和咱们在 Spring Task 的「2.6 应用配置文件」 提到的是一致的。
示例代码对应仓库: lab-28-task-quartz-memory 。
实际场景下,咱们必然须要考虑定时任务的高可用,因此基本上,确定使用 Quartz 的集群方案。所以本小节,咱们使用 Quartz 的 JDBC 存储器 JobStoreTX ,并是使用 MySQL 做为数据库。
以下是 Quartz 两种存储器的对比:
FROM https://blog.csdn.net/Evankak...
类型 | 优势 | 缺点 |
---|---|---|
RAMJobStore | 不要外部数据库,配置容易,运行速度快 | 由于调度程序信息是存储在被分配给 JVM 的内存里面,因此,当应用程序中止运行时,全部调度信息将被丢失。另外由于存储到JVM内存里面,因此能够存储多少个 Job 和 Trigger 将会受到限制 |
JDBC 做业存储 | 支持集群,由于全部的任务信息都会保存到数据库中,能够控制事物,还有就是若是应用服务器关闭或者重启,任务信息都不会丢失,而且能够恢复因服务器关闭或者重启而致使执行失败的任务 | 运行速度的快慢取决与链接数据库的快慢 |
艿艿:实际上,有方案能够实现兼具这两种方式的优势,咱们在 「666. 彩蛋」 中来讲。
另外,本小节提供的示例和 「3. 快速入门 Quartz 单机」 基本一致。😈 下面,让咱们开始遨游~
在 pom.xml
文件中,引入相关依赖。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.10.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>lab-28-task-quartz-jdbc</artifactId> <dependencies> <!-- 实现对数据库链接池的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <!-- 本示例,咱们使用 MySQL --> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <!-- 实现对 Spring MVC 的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 实现对 Quartz 的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <!-- 方便等会写单元测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
spring-boot-starter-test
依赖,等会会写两个单元测试方法。在 cn.iocoder.springboot.lab28.task.config.job
包路径下,建立 DemoJob01 和 DemoJob02 类。代码以下:
// DemoJob01.java @DisallowConcurrentExecution public class DemoJob01 extends QuartzJobBean { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private DemoService demoService; @Override protected void executeInternal(JobExecutionContext context) { logger.info("[executeInternal][我开始的执行了, demoService 为 ({})]", demoService); } } // DemoJob02.java @DisallowConcurrentExecution public class DemoJob02 extends QuartzJobBean { private Logger logger = LoggerFactory.getLogger(getClass()); @Override protected void executeInternal(JobExecutionContext context) { logger.info("[executeInternal][我开始的执行了]"); } }
@DisallowConcurrentExecution
注解,保证相同 JobDetail 在多个 JVM 进程中,有且仅有一个节点在执行。注意,不是以 Quartz Job 为维度,保证在多个 JVM 进程中,有且仅有一个节点在执行,而是以 JobDetail 为维度。虽说,绝大多数状况下,咱们会保证一个 Job 和 JobDetail 是一一对应。😈 因此,搞不清楚这个概念的胖友,最好搞清楚这个概念。实在有点懵逼,保证一个 Job 和 JobDetail 是一一对应就对了。
而 JobDetail 的惟一标识是 JobKey ,使用 name
+ group
两个属性。通常状况下,咱们只须要设置 name
便可,而 Quartz 会默认 group = DEFAULT
。
不过这里还有一点要补充,也是须要注意的,在 Quartz 中,相同 Scheduler 名字的节点,造成一个 Quartz 集群。在下文中,咱们能够经过 spring.quartz.scheduler-name
配置项,设置 Scheduler 的名字。
【重要】为何要说这个呢?由于咱们要完善一下上面的说法:经过在 Job 实现类上添加 @DisallowConcurrentExecution
注解,实如今相同 Quartz Scheduler 集群中,相同 JobKey 的 JobDetail ,保证在多个 JVM 进程中,有且仅有一个节点在执行。
在 application.yml
中,添加 Quartz 的配置,以下:
spring: datasource: user: url: jdbc:mysql://127.0.0.1:3306/lab-28-quartz-jdbc-user?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root password: quartz: url: jdbc:mysql://127.0.0.1:3306/lab-28-quartz-jdbc-quartz?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root password: # Quartz 的配置,对应 QuartzProperties 配置类 quartz: scheduler-name: clusteredScheduler # Scheduler 名字。默认为 schedulerName job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 auto-startup: true # Quartz 是否自动启动 startup-delay: 0 # 延迟 N 秒启动 wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true overwrite-existing-jobs: false # 是否覆盖已有 Job 的配置 properties: # 添加 Quartz Scheduler 附加属性,更多能够看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 org: quartz: # JobStore 相关配置 jobStore: # 数据源名称 dataSource: quartzDataSource # 使用的数据源 class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类 driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate tablePrefix: QRTZ_ # Quartz 表前缀 isClustered: true # 是集群模式 clusterCheckinInterval: 1000 useProperties: false # 线程池相关配置 threadPool: threadCount: 25 # 线程池大小。默认为 10 。 threadPriority: 5 # 线程优先级 class: org.quartz.simpl.SimpleThreadPool # 线程池类型 jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置 initialize-schema: never # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,咱们手动建立表结构。
在 spring.datasource
配置项下,用于建立多个数据源的配置。
user
配置,链接 lab-28-quartz-jdbc-user
库。目的是,为了模拟咱们通常项目,使用到的业务数据库。quartz
配置,链接 lab-28-quartz-jdbc-quartz
库。目的是,Quartz 会使用单独的数据库。😈 若是咱们有多个项目须要使用到 Quartz 数据库的话,能够统一使用一个,可是要注意配置 spring.quartz.scheduler-name
设置不一样的 Scheduler 名字,造成不一样的 Quartz 集群。在 spring.quartz
配置项下,额外增长了一些配置项,咱们逐个来看看。
scheduler-name
配置,Scheduler 名字。这个咱们在上文解释了不少次了,若是还不明白,请拍死本身。job-store-type
配置,设置了使用 "jdbc"
的 Job 存储器。properties.org.quartz.jobStore
配置,增长了 JobStore 相关配置。重点是,经过 dataSource
配置项,设置了使用名字为 "quartzDataSource"
的 DataSource 为数据源。😈 在 「4.4 DataSourceConfiguration」 中,咱们会使用 spring.datasource.quartz
配置,来建立该数据源。jdbc
配置项,虽然名字叫这个,主要是为了设置使用 SQL 初始化 Quartz 表结构。这里,咱们设置 initialize-schema = never
,咱们手动建立表结构。咳咳咳,配置项确实有点多。若是暂时搞不明白的胖友,能够先简单把 spring.datasource
数据源,修改为本身的便可。
在 Quartz Download 地址,下载对应版本的发布包。解压后,咱们能够在 src/org/quartz/impl/jdbcjobstore/
目录,看到各类数据库的 Quartz 表结构的初始化脚本。这里,由于咱们使用 MySQL ,因此使用 tables_mysql_innodb.sql
脚本。
在数据库中执行该脚本,完成初始化 Quartz 表结构。以下图所示:
关于每一个 Quartz 表结构的说明,能够看看 《Quartz 框架(二)——JobStore 数据库表字段详解》 文章。😈 实际上,也能够不看,哈哈哈哈。
咱们会发现,每一个表都有一个 SCHED_NAME
字段,Quartz Scheduler 名字。这样,实现每一个 Quartz 集群,数据层面的拆分。
在 cn.iocoder.springboot.lab28.task.config
包路径下,建立 DataSourceConfiguration 类,配置数据源。代码以下:
// DataSourceConfiguration.java @Configuration public class DataSourceConfiguration { /** * 建立 user 数据源的配置对象 */ @Primary @Bean(name = "userDataSourceProperties") @ConfigurationProperties(prefix = "spring.datasource.user") // 读取 spring.datasource.user 配置到 DataSourceProperties 对象 public DataSourceProperties userDataSourceProperties() { return new DataSourceProperties(); } /** * 建立 user 数据源 */ @Primary @Bean(name = "userDataSource") @ConfigurationProperties(prefix = "spring.datasource.user.hikari") // 读取 spring.datasource.user 配置到 HikariDataSource 对象 public DataSource userDataSource() { // 得到 DataSourceProperties 对象 DataSourceProperties properties = this.userDataSourceProperties(); // 建立 HikariDataSource 对象 return createHikariDataSource(properties); } /** * 建立 quartz 数据源的配置对象 */ @Bean(name = "quartzDataSourceProperties") @ConfigurationProperties(prefix = "spring.datasource.quartz") // 读取 spring.datasource.quartz 配置到 DataSourceProperties 对象 public DataSourceProperties quartzDataSourceProperties() { return new DataSourceProperties(); } /** * 建立 quartz 数据源 */ @Bean(name = "quartzDataSource") @ConfigurationProperties(prefix = "spring.datasource.quartz.hikari") @QuartzDataSource public DataSource quartzDataSource() { // 得到 DataSourceProperties 对象 DataSourceProperties properties = this.quartzDataSourceProperties(); // 建立 HikariDataSource 对象 return createHikariDataSource(properties); } private static HikariDataSource createHikariDataSource(DataSourceProperties properties) { // 建立 HikariDataSource 对象 HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); // 设置线程池名 if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } }
spring.datasource.user
配置项,建立了名字为 "userDataSource"
的 DataSource Bean 。而且,在其上咱们添加了 @Primay
注解,表示其是主数据源。spring.datasource.quartz
配置项,建立了名字为 "quartzDataSource"
的 DataSource Bean 。而且,在其上咱们添加了 @QuartzDataSource
注解,表示其是 Quartz 的数据源。😈 注意,必定要配置啊,这里艿艿卡了很久!!!!完成上述的工做以后,咱们须要配置 Quartz 的定时任务。目前,有两种方式:
在 cn.iocoder.springboot.lab28.task.config
包路径下,建立 ScheduleConfiguration 类,配置上述的两个示例 Job 。代码以下:
// ScheduleConfiguration.java @Configuration public class ScheduleConfiguration { public static class DemoJob01Configuration { @Bean public JobDetail demoJob01() { return JobBuilder.newJob(DemoJob01.class) .withIdentity("demoJob01") // 名字为 demoJob01 .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。由于建立 JobDetail 时,还没 Trigger 指向它,因此须要设置为 true ,表示保留。 .build(); } @Bean public Trigger demoJob01Trigger() { // 简单的调度计划的构造器 SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(5) // 频率。 .repeatForever(); // 次数。 // Trigger 构造器 return TriggerBuilder.newTrigger() .forJob(demoJob01()) // 对应 Job 为 demoJob01 .withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder .build(); } } public static class DemoJob02Configuration { @Bean public JobDetail demoJob02() { return JobBuilder.newJob(DemoJob02.class) .withIdentity("demoJob02") // 名字为 demoJob02 .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。由于建立 JobDetail 时,还没 Trigger 指向它,因此须要设置为 true ,表示保留。 .build(); } @Bean public Trigger demoJob02Trigger() { // 简单的调度计划的构造器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *"); // Trigger 构造器 return TriggerBuilder.newTrigger() .forJob(demoJob02()) // 对应 Job 为 demoJob02 .withIdentity("demoJob02Trigger") // 名字为 demoJob02Trigger .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder .build(); } } }
在 Quartz 调度器启动的时候,会根据该配置,自动调用以下方法:
Scheduler#addJob(JobDetail jobDetail, boolean replace)
方法,将 JobDetail 持久化到数据库。Scheduler#scheduleJob(Trigger trigger)
方法,将 Trigger 持久化到数据库。通常状况下,艿艿推荐使用 Scheduler 手动设置。
建立 QuartzSchedulerTest 类,建立分别添加 DemoJob01 和 DemoJob02 的 Quartz 定时任务配置。代码以下:
// QuartzSchedulerTest.java @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) public class QuartzSchedulerTest { @Autowired private Scheduler scheduler; @Test public void addDemoJob01Config() throws SchedulerException { // 建立 JobDetail JobDetail jobDetail = JobBuilder.newJob(DemoJob01.class) .withIdentity("demoJob01") // 名字为 demoJob01 .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。由于建立 JobDetail 时,还没 Trigger 指向它,因此须要设置为 true ,表示保留。 .build(); // 建立 Trigger SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(5) // 频率。 .repeatForever(); // 次数。 Trigger trigger = TriggerBuilder.newTrigger() .forJob(jobDetail) // 对应 Job 为 demoJob01 .withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder .build(); // 添加调度任务 scheduler.scheduleJob(jobDetail, trigger); // scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true); } @Test public void addDemoJob02Config() throws SchedulerException { // 建立 JobDetail JobDetail jobDetail = JobBuilder.newJob(DemoJob02.class) .withIdentity("demoJob02") // 名字为 demoJob02 .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。由于建立 JobDetail 时,还没 Trigger 指向它,因此须要设置为 true ,表示保留。 .build(); // 建立 Trigger CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *"); Trigger trigger = TriggerBuilder.newTrigger() .forJob(jobDetail) // 对应 Job 为 demoJob01 .withIdentity("demoJob02Trigger") // 名字为 demoJob01Trigger .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder .build(); // 添加调度任务 scheduler.scheduleJob(jobDetail, trigger); // scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true); } }
Scheduler#scheduleJob(JobDetail jobDetail, Trigger trigger)
方法,将 JobDetail 和 Trigger 持久化到数据库。Scheduler#scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace)
方法,传入 replace = true
进行覆盖配置。建立 Application.java
类,配置 @SpringBootApplication
注解便可。代码以下:
// Application.java @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
若是胖友想要测试集群下的运行状况,能够再建立 建立 Application02.java
类,配置 @SpringBootApplication
注解便可。代码以下:
// Application02.java @SpringBootApplication public class Application02 { public static void main(String[] args) { // 设置 Tomcat 随机端口 System.setProperty("server.port", "0"); // 启动 Spring Boot 应用 SpringApplication.run(Application.class, args); } }
示例代码对应仓库: lab-28-task-xxl-job 。
虽说,Quartz 的功能,已经可以知足咱们对定时任务的诉求,可是距离生产可用、好用,仍是有必定的距离。在艿艿最先开始实习的时候,由于Quartz 只提供了任务调度的功能,不提供管理任务的管理与监控控制台,须要本身去作二次封装。当时,由于社区中找不到合适的实现这块功能的开源项目,因此咱们就本身进行了简单的封装,知足咱们的管理与监控的需求。
不过如今呢,开源社区中已经有了不少优秀的调度任务中间件。其中,比较有表明性的就是 XXL-JOB 。其对本身的定义以下:
XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。
对于 XXL-JOB 的入门,艿艿已经在 《芋道 XXL-JOB 极简入门》 中编写,胖友先跳转到该文章阅读。重点是,要先搭建一个 XXL-JOB 调度中心。😈 由于,本文咱们是来在 Spring Boot 项目中,实现一个 XXL-JOB 执行器。
在 pom.xml
文件中,引入相关依赖。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>lab-28-task-xxl-job</artifactId> <dependencies> <!-- 实现对 Spring MVC 的自动化配置 --> <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.1.1</version> </dependency> </dependencies> </project>
具体每一个依赖的做用,胖友本身认真看下艿艿添加的全部注释噢。比较惋惜的是,目前 XXL-JOB 官方并未提供 Spring Boot Starter 包,略微有点尴尬。不过,社区已经有人在提交 Pull Request 了,详细可见 https://github.com/xuxueli/xx... 。
在 application.yml
中,添加 Quartz 的配置,以下:
server: port: 9090 #指定一个端口,避免和 XXL-JOB 调度中心的端口冲突。仅仅测试之用 # xxl-job xxl: job: admin: addresses: http://127.0.0.1:8080/xxl-job-admin # 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; executor: appname: lab-28-executor # 执行器 AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 ip: # 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅做为通信实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"; port: 6666 # ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不一样执行器端口; logpath: /Users/yunai/logs/xxl-job/lab-28-executor # 执行器运行日志文件存储磁盘路径 [选填] :须要对该路径拥有读写权限;为空则使用默认路径; logretentiondays: 30 # 执行器日志文件保存天数 [选填] : 过时日志自动清理, 限制值大于等于3时生效; 不然, 如-1, 关闭自动清理功能; accessToken: yudaoyuanma # 执行器通信TOKEN [选填]:非空时启用;
在 cn.iocoder.springboot.lab28.task.config
包路径下,建立 DataSourceConfiguration 类,配置 XXL-JOB 执行器。代码以下:
// XxlJobConfiguration.java @Configuration public class XxlJobConfiguration { @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; @Value("${xxl.job.executor.logretentiondays}") private int logRetentionDays; @Bean public XxlJobSpringExecutor xxlJobExecutor() { // 建立 XxlJobSpringExecutor 执行器 XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppName(appName); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); // 返回 return xxlJobSpringExecutor; } }
#xxlJobExecutor()
方法,建立了 Spring 容器下的 XXL-JOB 执行器 Bean 对象。要注意,方法上添加了的 @Bean
注解,配置了启动和销毁方法。在 cn.iocoder.springboot.lab28.task.job
包路径下,建立 DemoJob 类,示例定时任务类。代码以下:
// DemoJob.java @Component @JobHandler("demoJob") public class DemoJob extends IJobHandler { private Logger logger = LoggerFactory.getLogger(getClass()); private final AtomicInteger counts = new AtomicInteger(); @Override public ReturnT<String> execute(String param) throws Exception { // 打印日志 logger.info("[execute][定时第 ({}) 次执行]", counts.incrementAndGet()); // 返回执行成功 return ReturnT.SUCCESS; } }
#execute(String param)
方法,从而实现定时任务的逻辑。@JobHandler
注解,设置 JobHandler 的名字。后续,咱们在调度中心的控制台中,新增任务时,须要使用到这个名字。#execute(String param)
方法的返回结果,为 ReturnT 类型。当返回值符合 “ReturnT.code == ReturnT.SUCCESS_CODE”
时表示任务执行成功,不然表示任务执行失败,并且能够经过 “ReturnT.msg”
回调错误信息给调度中心;从而,在任务逻辑中能够方便的控制任务执行结果。
#execute(String param)
方法的方法参数,为调度中心的控制台中,新增任务时,配置的“任务参数”。通常状况下,不会使用到。
建立 Application.java
类,配置 @SpringBootApplication
注解便可。代码以下:
// Application.java @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
运行 Application 类,启动示例项目。输出日志精简以下:
# XXL-JOB 启动日志 2019-11-29 00:58:42.429 INFO 46957 --- [ main] c.xxl.job.core.executor.XxlJobExecutor : >>>>>>>>>>> xxl-job register jobhandler success, name:demoJob, jobHandler:cn.iocoder.springboot.lab28.task.job.DemoJob@3af9aa66 2019-11-29 00:58:42.451 INFO 46957 --- [ main] c.x.r.r.provider.XxlRpcProviderFactory : >>>>>>>>>>> xxl-rpc, provider factory add service success. serviceKey = com.xxl.job.core.biz.ExecutorBiz, serviceBean = class com.xxl.job.core.biz.impl.ExecutorBizImpl 2019-11-29 00:58:42.454 INFO 46957 --- [ main] c.x.r.r.provider.XxlRpcProviderFactory : >>>>>>>>>>> xxl-rpc, provider factory add service success. serviceKey = com.xxl.job.core.biz.ExecutorBiz, serviceBean = class com.xxl.job.core.biz.impl.ExecutorBizImpl 2019-11-29 00:58:42.565 INFO 46957 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2019-11-29 00:58:42.629 INFO 46957 --- [ Thread-7] com.xxl.rpc.remoting.net.Server : >>>>>>>>>>> xxl-rpc remoting server start success, nettype = com.xxl.rpc.remoting.net.impl.netty_http.server.NettyHttpServer, port = 6666
此时,由于咱们并未在 XXL-JOB 调度中心进行相关的配置,因此 DemoJob 并不会执行。下面,让咱们在 XXL-JOB 调度中心进行相应的配置。
浏览器打开 http://127.0.0.1:8080/xxl-job... 地址,即「执行器管理」菜单。以下图:
点击「新增执行器」按钮,弹出「新增执行器」界面。以下图:
填写完 "lab-28-executor"
执行器的信息,点击「保存」按钮,进行保存。耐心等待一会,执行器会自动注册上来。以下图:
相同执行器,有且仅需配置一次便可。
浏览器打开 http://127.0.0.1:8080/xxl-job... 地址,即「任务管理」菜单。以下图:
点击最右边的「新增」按钮,弹出「新增」界面。以下图:
填写完 "demoJob"
任务的信息,点击「保存」按钮,进行保存。以下图:
点击 "demoJob"
任务的「操做」按钮,选择「启动」,确认后,该 "demoJob"
任务的状态就变成了 RUNNING 。以下图:
此时,咱们打开执行器的 IDE 界面,能够看到 DemoJob 已经在每分钟执行一次了。日志以下:
2019-11-29 01:30:00.161 INFO 48374 --- [ Thread-18] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (1) 次执行] 2019-11-29 01:31:00.012 INFO 48374 --- [ Thread-18] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (2) 次执行] 2019-11-29 01:32:00.009 INFO 48374 --- [ Thread-18] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (3) 次执行] 2019-11-29 01:33:00.010 INFO 48374 --- [ Thread-18] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (4) 次执行] 2019-11-29 01:34:00.005 INFO 48374 --- [ Thread-18] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (5) 次执行]
而且,咱们在调度中心的界面上,点击 "demoJob"
任务的「操做」按钮,选择「查询日志」,能够看到相应的调度日志。以下图:
至此,咱们已经完成了 XXL-JOB 执行器的入门。
可能不少胖友不了解 Elastic-Job 这个中间件。咱们看一段其官方文档的介绍:
Elastic-Job 是一个分布式调度解决方案,由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成。Elastic-Job-Lite 定位为轻量级无中心化解决方案,使用 jar 包的形式提供分布式任务的协调服务。
Elastic-Job 基本是国内开源最好的调度任务中间件的几个中间件,可能没有之一,嘿嘿。目前处于有点“断更”的状态,具体可见 https://github.com/elasticjob... 。
因此关于这块的示例,艿艿暂时先不提供。若是对 Elastic-Job 源码感兴趣的胖友,能够看看艿艿写的以下两个系列:
① 如何选择?
可能胖友但愿了解下不一样调度中间件的对比。表格以下:
特性 | quartz | elastic-job-lite | xxl-job | LTS |
---|---|---|---|---|
依赖 | MySQL、jdk | jdk、zookeeper | mysql、jdk | jdk、zookeeper、maven |
高可用 | 多节点部署,经过竞争数据库锁来保证只有一个节点执行任务 | 经过zookeeper的注册与发现,能够动态的添加服务器 | 基于竞争数据库锁保证只有一个节点执行任务,支持水平扩容。能够手动增长定时任务,启动和暂停任务,有监控 | 集群部署,能够动态的添加服务器。能够手动增长定时任务,启动和暂停任务。有监控 |
任务分片 | × | √ | √ | √ |
管理界面 | × | √ | √ | √ |
难易程度 | 简单 | 简单 | 简单 | 略复杂 |
高级功能 | - | 弹性扩容,多种做业模式,失效转移,运行状态收集,多线程处理数据,幂等性,容错处理,spring命名空间支持 | 弹性扩容,分片广播,故障转移,Rolling实时日志,GLUE(支持在线编辑代码,免发布),任务进度监控,任务依赖,数据加密,邮件报警,运行报表,国际化 | 支持spring,spring boot,业务日志记录器,SPI扩展支持,故障转移,节点监控,多样化任务执行结果支持,FailStore容错,动态扩容。 |
版本更新 | 半年没更新 | 2年没更新 | 最近有更新 | 1年没更新 |
也推荐看看以下文章:
目前的情况,若是真的不知道怎么选择,能够先尝试下 XXL-JOB 。
② 中心化 V.S 去中心化
下面,让咱们一块儿来简单聊聊分布式调度中间件的实现方式的分类。一个分布式的调度中间件,会存在两种角色:
那么,若是从调度系统的角度来看,能够分红两类:
如此可知 XXL-Job 属于中心化的任务调度平台。目前采用这种方案的还有:
去中心化的任务调度平台,目前有:
艿艿:若是胖友想要更加的理解,能够看看艿艿朋友写的 《中心化 V.S 去中心化调度设计》
③ 任务竞争 V.S 任务预分配
那么,若是从任务分配的角度来看,能够分红两类:
如此可知 XXL-Job 属于任务竞争的任务调度平台。目前采用这种方案的还有:
任务预分配的任务调度平台,目前有:
通常来讲,基于任务预分配的任务调度平台,都会选择使用 Zookeeper 来协调分配任务到不一样的节点上。同时,任务调度平台必须是去中心化的方案,每一个节点便是调度器又是执行器。这样,任务在预分配在每一个节点以后,后续就本身调度给本身执行。
相比较而言,随着节点愈来愈多,基于任务竞争的方案会由于任务竞争,致使存在性能下滑的问题。而基于任务预分配的方案,则不会存在这个问题。而且,基于任务预分配的方案,性能会优于基于任务竞争的方案。
这里在推荐一篇 Elastic Job 开发者张亮的文章 《详解当当网的分布式做业框架 elastic-job》 ,灰常给力!
④ Quartz 是个优秀的调度内核
绝大多数状况下,咱们不会直接使用 Quartz 做为咱们的调度中间件的选择。可是,基本全部的分布式调度中间件,都将 Quartz 做为调度内核,由于 Quartz 在单纯任务调度自己提供了很强的功能。
不过呢,随着一个分布式调度中间件的逐步完善,又会逐步考虑抛弃 Quartz 做为调度内核,转而自研。例如说 XXL-JOB 在 2.1.0 RELEASE 的版本中,就已经更换成自研的调度模块。其替换的理由以下:
XXL-JOB 最终选择自研调度组件(早期调度组件基于 Quartz);
- 一方面,是为了精简系统下降冗余依赖。
- 另外一方面,是为了提供系统的可控度与稳定性。
在 Elastic-Job 3.X 的开发计划中,也有一项计划,就是自研调度组件,替换掉 Quartz 。