定时任务其实是非常普遍的需求,例如凌晨去执行一些脚本,去更新或者统计一些数据等等。

1. SpringBoot 实现定时任务

1.1 实例

第一点主要是针对 SpringBoot 原生去看看如何实现定时任务,当然了肯定会有一些更加成熟的定时任务解决方案,我们后面也会逐步去提,先一步一步看看,SpringBoot 这种实现的优劣。

一个特别简单的例子

10 秒钟执行一次某个方法,我们只需要通过 @EnableScheduling + @Scheduled 注解就妥了

  1. @Slf4j
  2. @Component
  3. @EnableScheduling // 放在启动类也可
  4. public class Test1 {
  5. /**
  6. * 每隔10秒执行 task1
  7. */
  8. @Scheduled(cron = "*/10 * * * * ?")
  9. public void task1() {
  10. try {
  11. log.info("定时任务 开始...");
  12. Thread.sleep(5 * 1000L);
  13. log.info("定时任务 结束...");
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }

运行结果:

  1. 2022-03-01 16:42:20.001 INFO 7550 --- [ scheduling-1] cn.ideal.task.task.Test1 : 定时任务 开始...
  2. 2022-03-01 16:42:25.004 INFO 7550 --- [ scheduling-1] cn.ideal.task.task.Test1 : 定时任务 结束...
  3. 2022-03-01 16:42:30.001 INFO 7550 --- [ scheduling-1] cn.ideal.task.task.Test1 : 定时任务 开始...
  4. 2022-03-01 16:42:35.002 INFO 7550 --- [ scheduling-1] cn.ideal.task.task.Test1 : 定时任务 结束...

Scheduled 的属性

  1. @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. @Repeatable(Schedules.class)
  5. public @interface Scheduled {
  6. String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;
  7. // 这个最常用,下面单独说
  8. String cron() default "";
  9. String zone() default "";
  10. // 距离上次结束的时间,即上次任务结束了才执行
  11. long fixedDelay() default -1;
  12. String fixedDelayString() default "";
  13. // 距离上一次开始的时间,如果上次任务执行时间过长,单线程下可能会耽误后面的任务。
  14. long fixedRate() default -1;
  15. String fixedRateString() default "";
  16. // 启动服务多久后执行第一次 可配合前面几个一起使用
  17. long initialDelay() default -1;
  18. String initialDelayString() default "";
  19. }

corn(常用)

https://www.bejson.com/othertools/cron/

  1. 0/2 * * * * ? 表示每2 执行任务
  2. 0 0/2 * * * ? 表示每2分钟 执行任务
  3. 0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
  4. 0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
  5. 0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
  6. 0 0 10,14,16 * * ? 每天上午10点,下午2点,4
  7. 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
  8. 0 0 12 ? * WED 表示每个星期三中午12
  9. 0 0 12 * * ? 每天中午12点触发
  10. 0 15 10 ? * * 每天上午10:15触发
  11. 0 15 10 * * ? 每天上午10:15触发
  12. 0 15 10 * * ? 每天上午10:15触发
  13. 0 15 10 * * ? 2005 2005年的每天上午10:15触发
  14. 0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
  15. 0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
  16. 0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
  17. 0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
  18. 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:102:44触发
  19. 0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
  20. 0 15 10 15 * ? 每月15日上午10:15触发
  21. 0 15 10 L * ? 每月最后一日的上午10:15触发
  22. 0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
  23. 0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
  24. 0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发

1.2 多线程

下面这个代码时典型的错误,我设置了三个定时任务,但是因为 SpringBoot 定时任务默认是单线程的,所以只能依次执行,而且,如果其中某一个卡死了,就会导致整个定时任务被阻塞。

  1. @Slf4j
  2. @Component
  3. @EnableScheduling
  4. public class Test1 {
  5. /**
  6. * 每隔10秒执行 task1
  7. */
  8. @Scheduled(cron = "*/10 * * * * ?")
  9. public void task1() {
  10. try {
  11. log.info("定时任务1 开始...");
  12. Thread.sleep(5 * 1000L);
  13. log.info("定时任务1 结束...");
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. /**
  19. * 每隔10秒执行 task2
  20. */
  21. @Scheduled(cron = "*/10 * * * * ?")
  22. public void task2() {
  23. try {
  24. log.info("定时任务2 开始...");
  25. Thread.sleep(5 * 1000L);
  26. log.info("定时任务2 结束...");
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. /**
  32. * 每隔10秒执行 task3
  33. */
  34. @Scheduled(cron = "*/10 * * * * ?")
  35. public void task3() {
  36. try {
  37. log.info("定时任务3 开始...");
  38. Thread.sleep(5 * 1000L);
  39. log.info("定时任务3 结束...");
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. }

1.3 多线程的坑

配置线程池,但是下面也是有一个坑,其中虽然看起来三个任务可以一起执行了,但其实是因为我们不小心加了 @EnableAsync 注解,其实只是异步化了,所以看起来快了,实际还是单线程。当你把 @EnableAsync 去掉,则马上就看不到效果了。

  1. @Slf4j
  2. @EnableAsync // 坑
  3. @Configuration
  4. public class ThreadPoolTaskConfig implements WebMvcConfigurer {
  5. @Bean("taskExecutor")
  6. public Executor taskExecutor() {
  7. ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  8. executor.setCorePoolSize(3);
  9. executor.setMaxPoolSize(3);
  10. executor.setQueueCapacity(5);
  11. executor.setKeepAliveSeconds(10);
  12. executor.setThreadNamePrefix("async-task-");
  13. // 线程池对拒绝任务的处理策略
  14. executor.setRejectedExecutionHandler(new RejectedExecutionHandler(){
  15. /**
  16. * 自定义线程池拒绝策略(这里可以发送告警邮件等等)
  17. */
  18. @Override
  19. public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
  20. log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",
  21. r.toString(),
  22. executor.getQueue().size());
  23. }
  24. });
  25. // 初始化
  26. executor.initialize();
  27. return executor;
  28. }
  29. }
  1. @Slf4j
  2. @Component
  3. @EnableScheduling
  4. public class Test1 {
  5. /**
  6. * 每隔10秒执行 task1
  7. */
  8. @Scheduled(cron = "*/10 * * * * ?")
  9. public void task1() {
  10. ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();
  11. Executor executor = thread.taskExecutor();
  12. executor.execute(new Runnable() {
  13. @Override
  14. public void run() {
  15. try {
  16. log.info("定时任务1 开始...");
  17. Thread.sleep(5 * 1000L);
  18. log.info("定时任务1 结束...");
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. });
  24. }
  25. /**
  26. * 每隔10秒执行 task2
  27. */
  28. @Scheduled(cron = "*/10 * * * * ?")
  29. public void task2() {
  30. ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();
  31. Executor executor = thread.taskExecutor();
  32. executor.execute(new Runnable() {
  33. @Override
  34. public void run() {
  35. try {
  36. log.info("定时任务2 开始...");
  37. Thread.sleep(5 * 1000L);
  38. log.info("定时任务2 结束...");
  39. } catch (InterruptedException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. });
  44. }
  45. /**
  46. * 每隔10秒执行 task3
  47. */
  48. @Scheduled(cron = "*/10 * * * * ?")
  49. public void task3() {
  50. ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();
  51. Executor executor = thread.taskExecutor();
  52. executor.execute(new Runnable() {
  53. @Override
  54. public void run() {
  55. try {
  56. log.info("定时任务3 开始...");
  57. Thread.sleep(5 * 1000L);
  58. log.info("定时任务3 结束...");
  59. } catch (InterruptedException e) {
  60. e.printStackTrace();
  61. }
  62. }
  63. });
  64. }
  65. }

1.4 正确配置的三种方法:

1.4.1 方式1

重写SchedulingConfigurer#configureTasks()

这种方式,在测试类型方法上加上 @Scheduled 注解就好了。设置一下执行频率等。其中这个内部类也可以单独写出去。

其实它最大的特色是可以实现动态的修改,可以百度下,感觉还是比较麻烦的

https://mp.weixin.qq.com/s/lFUReSuVoQ_kWbAi0ANAoQ

  1. @Slf4j
  2. @Configuration
  3. public class ThreadPoolTaskConfig implements WebMvcConfigurer {
  4. @Bean
  5. public SchedulingConfigurer schedulingConfigurer() {
  6. return new MySchedulingConfigurer();
  7. }
  8. static class MySchedulingConfigurer implements SchedulingConfigurer {
  9. @Override
  10. public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
  11. ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
  12. taskScheduler.setPoolSize(3);
  13. taskScheduler.setThreadNamePrefix("schedule-task-");
  14. taskScheduler.setRejectedExecutionHandler(
  15. new RejectedExecutionHandler() {
  16. /**
  17. * 自定义线程池拒绝策略(模拟发送告警邮件)
  18. */
  19. @Override
  20. public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
  21. log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",
  22. r.toString(),
  23. executor.getQueue().size());
  24. }
  25. });
  26. taskScheduler.initialize();
  27. taskRegistrar.setScheduler(taskScheduler);
  28. }
  29. }
  30. }

1.4.2 方式2

@Bean + ThreadPoolTaskScheduler

  1. @Slf4j
  2. @Configuration
  3. public class ThreadPoolTaskConfig implements WebMvcConfigurer {
  4. @Bean("taskExecutor")
  5. public Executor taskExecutor() {
  6. ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
  7. taskScheduler.setPoolSize(3);
  8. taskScheduler.setThreadNamePrefix("async-task-");
  9. // 线程池对拒绝任务的处理策略
  10. taskScheduler.setRejectedExecutionHandler(new RejectedExecutionHandler(){
  11. /**
  12. * 自定义线程池拒绝策略(这里可以发送告警邮件等等)
  13. */
  14. @Override
  15. public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
  16. log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",
  17. r.toString(),
  18. executor.getQueue().size());
  19. }
  20. });
  21. // 初始化
  22. taskScheduler.initialize();
  23. return taskScheduler;
  24. }
  25. }
  1. /**
  2. * 每隔10秒执行 task1
  3. */
  4. @Scheduled(cron = "*/10 * * * * ?")
  5. public void task1() {
  6. ThreadPoolTaskConfig thread = new ThreadPoolTaskConfig();
  7. Executor executor = thread.taskExecutor();
  8. executor.execute(new Runnable() {
  9. @Override
  10. public void run() {
  11. try {
  12. log.info("定时任务1 开始...");
  13. Thread.sleep(5 * 1000L);
  14. log.info("定时任务1 结束...");
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. });
  20. }

1.4.3 方式3

@Bean + ScheduledThreadPoolExecutor

  1. @Slf4j
  2. @Configuration
  3. public class ThreadPoolTaskConfig implements WebMvcConfigurer {
  4. // 方式3
  5. @Bean("taskScheduler")
  6. public Executor taskScheduler() {
  7. ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
  8. 3,
  9. new RejectedExecutionHandler() {
  10. /**
  11. * 自定义线程池拒绝策略(模拟发送告警邮件)
  12. */
  13. @Override
  14. public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
  15. log.info("定时任务出错 当前线程名称为:{}, 当前线程池队列长度为:{}",
  16. r.toString(),
  17. executor.getQueue().size());
  18. }
  19. }
  20. );
  21. executor.setMaximumPoolSize(5);
  22. executor.setKeepAliveTime(60, TimeUnit.SECONDS);
  23. return executor;
  24. }
  25. }

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 中的端口配置,访问

登录后如图所示

image.png

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

image.png

2.2 代码引入

打开你需要定时任务服务

引入依赖

  1. <dependency>
  2. <groupId>com.xuxueli</groupId>
  3. <artifactId>xxl-job-core</artifactId>
  4. <version>2.3.0</version>
  5. </dependency>

修改 properties,注意我是在本地演示的,所以大家一定修改成自己的

  1. # xxl-job配置地址
  2. xxl.job.admin.addresses = http://127.0.0.1:8080/xxl-job-admin
  3. # 平台设置步骤中新建的执行器名称(通常与项目绑定)
  4. xxl.job.executor.appname = task-test-01
  5. # 执行器ip,即本项目部署ip [选填,为空时自动获取]
  6. xxl.job.executor.ip = 127.0.0.1
  7. # 执行器端口 [选填,默认9999]
  8. xxl.job.executor.port = 9999
  9. # 日志地址
  10. xxl.job.executor.logpath = /Users/ideal/develop/study/Task
  11. # 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
  12. xxl.job.executor.logretentiondays = 30

创建 Config 类

  1. package cn.ideal.config;
  2. import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
  3. import org.springframework.beans.factory.annotation.Value;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. /**
  7. * @author erjingzhi
  8. * @date 2022-03-02 10:45 上午
  9. **/
  10. @Configuration
  11. public class XxlJobConfig {
  12. @Value("${xxl.job.admin.addresses}")
  13. private String adminAddresses;
  14. @Value("${xxl.job.executor.appname}")
  15. private String appName;
  16. @Value("${xxl.job.executor.ip}")
  17. private String ip;
  18. @Value("${xxl.job.executor.port}")
  19. private int port;
  20. @Value("${xxl.job.executor.logpath}")
  21. private String logPath;
  22. @Value("${xxl.job.executor.logretentiondays}")
  23. private int logRetentionDays;
  24. @Bean(initMethod = "start", destroyMethod = "destroy")
  25. public XxlJobSpringExecutor xxlJobExecutor() {
  26. XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
  27. xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
  28. xxlJobSpringExecutor.setAppname(appName);
  29. xxlJobSpringExecutor.setIp(ip);
  30. xxlJobSpringExecutor.setPort(port);
  31. xxlJobSpringExecutor.setLogPath(logPath);
  32. xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
  33. return xxlJobSpringExecutor;
  34. }
  35. }

为任务配置 @XxlJob 注解,其中的字符串,就是这个匹配的关键字。运行时不可以修改

  1. @Slf4j
  2. @Component
  3. @EnableScheduling
  4. public class Test1 {
  5. @XxlJob("taskTest01")
  6. public void execute() {
  7. log.info("----XXL-JOB---- 任务执行");
  8. }
  9. }

然后启动服务,只要控制台能看到 XxlJob 等字样,就算启动成功了

这时再打开web控制台,找到任务管理,点击新建,然后配置哪个执行器,接着在 JobHandler 中匹配你前面在业务代码上写的那个注解字符串 @XxlJob(“taskTest01”),即 taskTest01,然后配置 cron 即可。

image.png

点击开始后,观察业务代码的日志。

  1. 2022-03-02 10:54:54.079 INFO 13437 --- [ Thread-22] cn.ideal.task.Test1 : ----XXL-JOB---- 任务执行
  2. 2022-03-02 10:54:57.021 INFO 13437 --- [ Thread-22] cn.ideal.task.Test1 : ----XXL-JOB---- 任务执行
  3. 2022-03-02 10:55:00.012 INFO 13437 --- [ Thread-22] cn.ideal.task.Test1 : ----XXL-JOB---- 任务执行
  4. 2022-03-02 10:55:03.014 INFO 13437 --- [ Thread-22] cn.ideal.task.Test1 : ----XXL-JOB---- 任务执行

确实成功了。