定时任务其实是非常普遍的需求,例如凌晨去执行一些脚本,去更新或者统计一些数据等等。
1. SpringBoot 实现定时任务
1.1 实例
第一点主要是针对 SpringBoot 原生去看看如何实现定时任务,当然了肯定会有一些更加成熟的定时任务解决方案,我们后面也会逐步去提,先一步一步看看,SpringBoot 这种实现的优劣。
一个特别简单的例子
10 秒钟执行一次某个方法,我们只需要通过 @EnableScheduling + @Scheduled 注解就妥了
@Slf4j@Component@EnableScheduling // 放在启动类也可public class Test1 {/*** 每隔10秒执行 task1*/@Scheduled(cron = "*/10 * * * * ?")public void task1() {try {log.info("定时任务 开始...");Thread.sleep(5 * 1000L);log.info("定时任务 结束...");} catch (InterruptedException e) {e.printStackTrace();}}}
运行结果:
2022-03-01 16:42:20.001 INFO 7550 --- [ scheduling-1] cn.ideal.task.task.Test1 : 定时任务 开始...2022-03-01 16:42:25.004 INFO 7550 --- [ scheduling-1] cn.ideal.task.task.Test1 : 定时任务 结束...2022-03-01 16:42:30.001 INFO 7550 --- [ scheduling-1] cn.ideal.task.task.Test1 : 定时任务 开始...2022-03-01 16:42:35.002 INFO 7550 --- [ scheduling-1] cn.ideal.task.task.Test1 : 定时任务 结束...
Scheduled 的属性
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Repeatable(Schedules.class)public @interface Scheduled {String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;// 这个最常用,下面单独说String cron() default "";String zone() default "";// 距离上次结束的时间,即上次任务结束了才执行long fixedDelay() default -1;String fixedDelayString() default "";// 距离上一次开始的时间,如果上次任务执行时间过长,单线程下可能会耽误后面的任务。long fixedRate() default -1;String fixedRateString() default "";// 启动服务多久后执行第一次 可配合前面几个一起使用long initialDelay() default -1;String initialDelayString() default "";}
corn(常用)
https://www.bejson.com/othertools/cron/
0/2 * * * * ? 表示每2秒 执行任务0 0/2 * * * ? 表示每2分钟 执行任务0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作0 0 10,14,16 * * ? 每天上午10点,下午2点,4点0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时0 0 12 ? * WED 表示每个星期三中午12点0 0 12 * * ? 每天中午12点触发0 15 10 ? * * 每天上午10:15触发0 15 10 * * ? 每天上午10:15触发0 15 10 * * ? 每天上午10:15触发0 15 10 * * ? 2005 2005年的每天上午10:15触发0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发0 15 10 ? * MON-FRI 周一至周五的上午10:15触发0 15 10 15 * ? 每月15日上午10:15触发0 15 10 L * ? 每月最后一日的上午10:15触发0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
1.2 多线程
下面这个代码时典型的错误,我设置了三个定时任务,但是因为 SpringBoot 定时任务默认是单线程的,所以只能依次执行,而且,如果其中某一个卡死了,就会导致整个定时任务被阻塞。
@Slf4j@Component@EnableSchedulingpublic class Test1 {/*** 每隔10秒执行 task1*/@Scheduled(cron = "*/10 * * * * ?")public void task1() {try {log.info("定时任务1 开始...");Thread.sleep(5 * 1000L);log.info("定时任务1 结束...");} catch (InterruptedException e) {e.printStackTrace();}}/*** 每隔10秒执行 task2*/@Scheduled(cron = "*/10 * * * * ?")public void task2() {try {log.info("定时任务2 开始...");Thread.sleep(5 * 1000L);log.info("定时任务2 结束...");} catch (InterruptedException e) {e.printStackTrace();}}/*** 每隔10秒执行 task3*/@Scheduled(cron = "*/10 * * * * ?")public void task3() {try {log.info("定时任务3 开始...");Thread.sleep(5 * 1000L);log.info("定时任务3 结束...");} catch (InterruptedException e) {e.printStackTrace();}}}
1.3 多线程的坑
配置线程池,但是下面也是有一个坑,其中虽然看起来三个任务可以一起执行了,但其实是因为我们不小心加了 @EnableAsync 注解,其实只是异步化了,所以看起来快了,实际还是单线程。当你把 @EnableAsync 去掉,则马上就看不到效果了。
@Slf4j@EnableAsync // 坑@Configurationpublic class ThreadPoolTaskConfig implements WebMvcConfigurer {@Bean("taskExecutor")public Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(3);executor.setMaxPoolSize(3);executor.setQueueCapacity(5);executor.setKeepAliveSeconds(10);executor.setThreadNamePrefix("async-task-");// 线程池对拒绝任务的处理策略executor.setRejectedExecutionHandler(new RejectedExecutionHandler(){/*** 自定义线程池拒绝策略(这里可以发送告警邮件等等)*/@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",r.toString(),executor.getQueue().size());}});// 初始化executor.initialize();return executor;}}
@Slf4j@Component@EnableSchedulingpublic class Test1 {/*** 每隔10秒执行 task1*/@Scheduled(cron = "*/10 * * * * ?")public void task1() {ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();Executor executor = thread.taskExecutor();executor.execute(new Runnable() {@Overridepublic void run() {try {log.info("定时任务1 开始...");Thread.sleep(5 * 1000L);log.info("定时任务1 结束...");} catch (InterruptedException e) {e.printStackTrace();}}});}/*** 每隔10秒执行 task2*/@Scheduled(cron = "*/10 * * * * ?")public void task2() {ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();Executor executor = thread.taskExecutor();executor.execute(new Runnable() {@Overridepublic void run() {try {log.info("定时任务2 开始...");Thread.sleep(5 * 1000L);log.info("定时任务2 结束...");} catch (InterruptedException e) {e.printStackTrace();}}});}/*** 每隔10秒执行 task3*/@Scheduled(cron = "*/10 * * * * ?")public void task3() {ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();Executor executor = thread.taskExecutor();executor.execute(new Runnable() {@Overridepublic void run() {try {log.info("定时任务3 开始...");Thread.sleep(5 * 1000L);log.info("定时任务3 结束...");} catch (InterruptedException e) {e.printStackTrace();}}});}}
1.4 正确配置的三种方法:
1.4.1 方式1
重写SchedulingConfigurer#configureTasks()
这种方式,在测试类型方法上加上 @Scheduled 注解就好了。设置一下执行频率等。其中这个内部类也可以单独写出去。
其实它最大的特色是可以实现动态的修改,可以百度下,感觉还是比较麻烦的
https://mp.weixin.qq.com/s/lFUReSuVoQ_kWbAi0ANAoQ
@Slf4j@Configurationpublic class ThreadPoolTaskConfig implements WebMvcConfigurer {@Beanpublic SchedulingConfigurer schedulingConfigurer() {return new MySchedulingConfigurer();}static class MySchedulingConfigurer implements SchedulingConfigurer {@Overridepublic void configureTasks(ScheduledTaskRegistrar taskRegistrar) {ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();taskScheduler.setPoolSize(3);taskScheduler.setThreadNamePrefix("schedule-task-");taskScheduler.setRejectedExecutionHandler(new RejectedExecutionHandler() {/*** 自定义线程池拒绝策略(模拟发送告警邮件)*/@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",r.toString(),executor.getQueue().size());}});taskScheduler.initialize();taskRegistrar.setScheduler(taskScheduler);}}}
1.4.2 方式2
@Bean + ThreadPoolTaskScheduler
@Slf4j@Configurationpublic class ThreadPoolTaskConfig implements WebMvcConfigurer {@Bean("taskExecutor")public Executor taskExecutor() {ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();taskScheduler.setPoolSize(3);taskScheduler.setThreadNamePrefix("async-task-");// 线程池对拒绝任务的处理策略taskScheduler.setRejectedExecutionHandler(new RejectedExecutionHandler(){/*** 自定义线程池拒绝策略(这里可以发送告警邮件等等)*/@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",r.toString(),executor.getQueue().size());}});// 初始化taskScheduler.initialize();return taskScheduler;}}
/*** 每隔10秒执行 task1*/@Scheduled(cron = "*/10 * * * * ?")public void task1() {ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();Executor executor = thread.taskExecutor();executor.execute(new Runnable() {@Overridepublic void run() {try {log.info("定时任务1 开始...");Thread.sleep(5 * 1000L);log.info("定时任务1 结束...");} catch (InterruptedException e) {e.printStackTrace();}}});}
1.4.3 方式3
@Bean + ScheduledThreadPoolExecutor
@Slf4j@Configurationpublic class ThreadPoolTaskConfig implements WebMvcConfigurer {// 方式3@Bean("taskScheduler")public Executor taskScheduler() {ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(3,new RejectedExecutionHandler() {/*** 自定义线程池拒绝策略(模拟发送告警邮件)*/@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",r.toString(),executor.getQueue().size());}});executor.setMaximumPoolSize(5);executor.setKeepAliveTime(60, TimeUnit.SECONDS);return executor;}}
2. 集成 Xxl-Job
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
总之非常方便
2.1 安装
地址:https://gitee.com/xuxueli0323/xxl-job
git 后,我这边选择了 2.3.0 的分支版本
将数据库文件导入 doc/db/tables_xxl_job.sql,然后将 xxl-job-admin 模块的 application.properties 文件中数据库的信息修改为自己的。
然后可以直接运行在本地了,当然也可以打成 jar 部署到云。
通过你在 properties 中的端口配置,访问
- 我是在本地的:http://localhost:8080/xxl-job-admin/
- 用户名 admin 密码 123456
登录后如图所示

然后配置执行器,新增一个,这个执行器的概念一般对应你的某个服务。注册方式选择自动录入,表示定时任务平台自动获取demo项目地址。

2.2 代码引入
打开你需要定时任务服务
引入依赖
<dependency><groupId>com.xuxueli</groupId><artifactId>xxl-job-core</artifactId><version>2.3.0</version></dependency>
修改 properties,注意我是在本地演示的,所以大家一定修改成自己的
# xxl-job配置地址xxl.job.admin.addresses = http://127.0.0.1:8080/xxl-job-admin# 平台设置步骤中新建的执行器名称(通常与项目绑定)xxl.job.executor.appname = task-test-01# 执行器ip,即本项目部署ip [选填,为空时自动获取]xxl.job.executor.ip = 127.0.0.1# 执行器端口 [选填,默认9999]xxl.job.executor.port = 9999# 日志地址xxl.job.executor.logpath = /Users/ideal/develop/study/Task# 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;xxl.job.executor.logretentiondays = 30
创建 Config 类
package cn.ideal.config;import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/*** @author erjingzhi* @date 2022-03-02 10:45 上午**/@Configurationpublic 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.executor.logpath}")private String logPath;@Value("${xxl.job.executor.logretentiondays}")private int logRetentionDays;@Bean(initMethod = "start", destroyMethod = "destroy")public XxlJobSpringExecutor xxlJobExecutor() {XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();xxlJobSpringExecutor.setAdminAddresses(adminAddresses);xxlJobSpringExecutor.setAppname(appName);xxlJobSpringExecutor.setIp(ip);xxlJobSpringExecutor.setPort(port);xxlJobSpringExecutor.setLogPath(logPath);xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);return xxlJobSpringExecutor;}}
为任务配置 @XxlJob 注解,其中的字符串,就是这个匹配的关键字。运行时不可以修改
@Slf4j@Component@EnableSchedulingpublic class Test1 {@XxlJob("taskTest01")public void execute() {log.info("----XXL-JOB---- 任务执行");}}
然后启动服务,只要控制台能看到 XxlJob 等字样,就算启动成功了
这时再打开web控制台,找到任务管理,点击新建,然后配置哪个执行器,接着在 JobHandler 中匹配你前面在业务代码上写的那个注解字符串 @XxlJob(“taskTest01”),即 taskTest01,然后配置 cron 即可。

点击开始后,观察业务代码的日志。
2022-03-02 10:54:54.079 INFO 13437 --- [ Thread-22] cn.ideal.task.Test1 : ----XXL-JOB---- 任务执行2022-03-02 10:54:57.021 INFO 13437 --- [ Thread-22] cn.ideal.task.Test1 : ----XXL-JOB---- 任务执行2022-03-02 10:55:00.012 INFO 13437 --- [ Thread-22] cn.ideal.task.Test1 : ----XXL-JOB---- 任务执行2022-03-02 10:55:03.014 INFO 13437 --- [ Thread-22] cn.ideal.task.Test1 : ----XXL-JOB---- 任务执行
确实成功了。
