定时任务其实是非常普遍的需求,例如凌晨去执行一些脚本,去更新或者统计一些数据等等。
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
@EnableScheduling
public 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 // 坑
@Configuration
public 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(){
/**
* 自定义线程池拒绝策略(这里可以发送告警邮件等等)
*/
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",
r.toString(),
executor.getQueue().size());
}
});
// 初始化
executor.initialize();
return executor;
}
}
@Slf4j
@Component
@EnableScheduling
public class Test1 {
/**
* 每隔10秒执行 task1
*/
@Scheduled(cron = "*/10 * * * * ?")
public void task1() {
ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();
Executor executor = thread.taskExecutor();
executor.execute(new Runnable() {
@Override
public 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() {
@Override
public 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() {
@Override
public 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
@Configuration
public class ThreadPoolTaskConfig implements WebMvcConfigurer {
@Bean
public SchedulingConfigurer schedulingConfigurer() {
return new MySchedulingConfigurer();
}
static class MySchedulingConfigurer implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(3);
taskScheduler.setThreadNamePrefix("schedule-task-");
taskScheduler.setRejectedExecutionHandler(
new RejectedExecutionHandler() {
/**
* 自定义线程池拒绝策略(模拟发送告警邮件)
*/
@Override
public 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
@Configuration
public class ThreadPoolTaskConfig implements WebMvcConfigurer {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(3);
taskScheduler.setThreadNamePrefix("async-task-");
// 线程池对拒绝任务的处理策略
taskScheduler.setRejectedExecutionHandler(new RejectedExecutionHandler(){
/**
* 自定义线程池拒绝策略(这里可以发送告警邮件等等)
*/
@Override
public 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() {
@Override
public 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
@Configuration
public class ThreadPoolTaskConfig implements WebMvcConfigurer {
// 方式3
@Bean("taskScheduler")
public Executor taskScheduler() {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
3,
new RejectedExecutionHandler() {
/**
* 自定义线程池拒绝策略(模拟发送告警邮件)
*/
@Override
public 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 上午
**/
@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.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
@EnableScheduling
public 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---- 任务执行
确实成功了。