springboot定时任务

正文

avatar

首先,文中使用的是 springboot的2.7.0版本。

定时任务用法-单线程

定时任务的配置很简单,只需要配置声明@EnableScheduling 就可以了,然后就可以使用了,大概就是下面的样子:

1
2
3
4
5
6
7
8
9
10

// 声明开启定时任务,@Scheduled注解才会生效
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

注解版本-cron表达式

新建一个类,使用Scheduled注解的cron,这里需要对cron了解,不过网上有在线版生成cron的网站。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

@Component
public class SchedulingTest {
   private  static final Logger log= LoggerFactory.getLogger(SchedulingTest.class);

   @Scheduled(cron = "*/5 * * * * ?")
   public void testOne(){
      try {
         Thread.sleep(1000*6);
         log.info("每五秒钟执行一次:时间:{},线程:{}",LocalDateTime.now(),Thread.currentThread().getName());
      }catch (Exception e){
         log.error("",e);
      }
   }
}
   

执行结果:

1
2
3
4
5

2022-08-28 10:49:36.011  INFO 54799 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:49:36.011,线程:scheduling-1
2022-08-28 10:49:46.010  INFO 54799 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:49:46.010,线程:scheduling-1
2022-08-28 10:49:56.010  INFO 54799 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:49:56.010,线程:scheduling-1

从执行结果可以看出来,执行时间是十秒钟一次,但是设置的又是五秒钟执行一次,原因是执行任务会休眠六秒钟,执行下一次任务时,任务还在休眠,所以中间那一次会被放弃。 至此,一个springboot自带的简单的定时任务就创建好了,可以执行了。

注解版本-fixedDelay与fixedRate

创建两个定时任务,分别使用fixedDelay与fixedRate,需要注意的是两个定时任务都加了线程睡眠六秒钟。

fixedDelay

先来看fixedDelay, 这个参数是在任务执行完成后,在进行时间延迟,具体例子如下:

1
2
3
4
5
6
7
8
9
10
11

    @Scheduled(fixedDelay = 5,timeUnit = TimeUnit.SECONDS)
    public void testOne(){
        try {
            Thread.sleep(1000*6);
            log.info("每五秒钟执行一次:时间:{},线程:{}",LocalDateTime.now(),Thread.currentThread().getName());
        }catch (Exception e){
            log.error("",e);
        }
    }
    

执行结果:

1
2
3
4
5
6

2022-08-28 10:35:08.763  INFO 54611 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:35:08.763,线程:scheduling-1
2022-08-28 10:35:19.777  INFO 54611 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:35:19.777,线程:scheduling-1
2022-08-28 10:35:30.784  INFO 54611 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:35:30.784,线程:scheduling-1
2022-08-28 10:35:41.791  INFO 54611 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:35:41.791,线程:scheduling-1

可以看到每次任务的间隔时间都是11秒,也就是任务执行了6秒加上原本设置的五秒,所以总共间隔是一秒。也就是fixedDelay是在任务执行完成后才开始计算设置的间隔时间。

fixedRate

例子如下:

1
2
3
4
5
6
7
8
9
10
11

    @Scheduled(fixedRate = 5,timeUnit = TimeUnit.SECONDS)
    public void testTwo(){
        try {
            Thread.sleep(1000*6);
            log.info("每五秒钟执行一次:时间:{},线程:{}",LocalDateTime.now(),Thread.currentThread().getName());
        }catch (Exception e){
            log.error("",e);
        }
    }
    

执行结果:

1
2
3
4
5

2022-08-28 10:40:02.805  INFO 54660 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:40:02.805,线程:scheduling-1
2022-08-28 10:40:08.812  INFO 54660 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:40:08.812,线程:scheduling-1
2022-08-28 10:40:14.817  INFO 54660 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : 每五秒钟执行一次:时间:2022-08-28T10:40:14.817,线程:scheduling-1

从执行结果中可以看到,fixedRate的计算时间是从任务开始执行时计算的,若任务执行时间超过了设置时间,则下一次会在任务完成后立即执行。

从上面fixedRate和fixedDelay可以看出区别,前者是从任务开始时计算设置的时间,后者是任务执行完成后再计算设置时间。

实现SchedulingConfigurer接口

定时任务也可以使用实现SchedulingConfigurer接口来实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class One implements SchedulingConfigurer {
    private static final Logger log= LoggerFactory.getLogger(One.class);
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(
                //1.添加任务内容(Runnable)
                () -> {
                    try {
                        log.info("One每五秒钟执行一次:时间:{},线程:{}",LocalDateTime.now(),Thread.currentThread().getName());
                        Thread.sleep(1000*6);
                    }catch (Exception e){
                        log.error("定时任务错误",e);
                    }
                },
                //2.设置执行周期(Trigger)
                triggerContext -> new CronTrigger("*/5 * * * * ?").nextExecutionTime(triggerContext)
        );
    }
}

执行结果:

1
2
3
4
5

2022-08-28 14:55:05.006  INFO 57146 --- [pool-1-thread-1] com.example.demo.scheduling.One          : One每五秒钟执行一次:时间:2022-08-28T14:55:05.006,线程:pool-1-thread-1
2022-08-28 14:55:15.005  INFO 57146 --- [pool-1-thread-1] com.example.demo.scheduling.One          : One每五秒钟执行一次:时间:2022-08-28T14:55:15.005,线程:pool-1-thread-1
2022-08-28 14:55:25.004  INFO 57146 --- [pool-1-thread-1] com.example.demo.scheduling.One          : One每五秒钟执行一次:时间:2022-08-28T14:55:25.004,线程:pool-1-thread-1

从执行结果可以看出,和cron表达式是一样的,当任务执行时间超过了设置时间,那么在执行时间以内的任务都不会执行,直到任务执行完才会执行下一次。

多线程与单线程区别

在上面的案例中,所有的定时任务都是用的一个线程在执行,因为sprin中默认对线程池配置了一个线程,如果只有一个简单的定时任务,或者定时任务之间没有交叉点的话,没啥影响。 但是当定时任务之间有时间交叉点,那么单线程的话就会受到影响了。 先来看看单线程的影响: 测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

@Component
public class SchedulingTest {
    private  static final Logger log= LoggerFactory.getLogger(SchedulingTest.class);
    @Scheduled(cron = "*/5 * * * * ?")
    public void testOne(){
        try {
            log.info("testOne每五秒钟执行一次:时间:{},线程:{}",LocalDateTime.now(),Thread.currentThread().getName());
            Thread.sleep(1000*4);
        }catch (Exception e){
            log.error("",e);
        }
    }
    @Scheduled(cron = "*/1 * * * * ?")
    public void testTwo(){
        try {
            log.info("testTwo每秒钟执行一次:时间:{},线程:{}",LocalDateTime.now(),Thread.currentThread().getName());
            Thread.sleep(999);
        }catch (Exception e){
            log.error(e.getMessage());
        }
    }
}

未配置多线程的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

2022-08-28 16:19:22.000  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:19:22,线程:scheduling-1
2022-08-28 16:19:24.000  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:19:24,线程:scheduling-1
2022-08-28 16:19:25.005  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:19:25.005,线程:scheduling-1
2022-08-28 16:19:29.011  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:19:29.011,线程:scheduling-1
2022-08-28 16:19:30.016  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:19:30.016,线程:scheduling-1
2022-08-28 16:19:34.020  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:19:34.020,线程:scheduling-1
2022-08-28 16:19:35.020  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:19:35.020,线程:scheduling-1
2022-08-28 16:19:39.027  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:19:39.027,线程:scheduling-1
2022-08-28 16:19:40.028  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:19:40.028,线程:scheduling-1
2022-08-28 16:19:44.034  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:19:44.034,线程:scheduling-1
2022-08-28 16:19:45.040  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:19:45.039,线程:scheduling-1
2022-08-28 16:19:49.043  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:19:49.043,线程:scheduling-1
2022-08-28 16:19:50.047  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:19:50.047,线程:scheduling-1
2022-08-28 16:19:54.053  INFO 58048 --- [   scheduling-1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:19:54.053,线程:scheduling-1

从结果可以看出,单线程的情况下,每秒执行一次的任务并没有按预想的执行,被每五秒执行的任务影响到了,下面来配置线程池有多个线程。

如果使用@Scheduled注解的话,就在配置文件添加配置就可以了,简单配置如下:

1
2
3
4
5
6
7
8

spring:
  task:
    scheduling:
      pool:
        size: 4
      thread-name-prefix: scheduling--
      

还是上面部分代码, 只是配置了多线程,执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12

2022-08-28 16:23:44.001  INFO 58093 --- [  scheduling--1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:23:44.001,线程:scheduling--1
2022-08-28 16:23:45.005  INFO 58093 --- [  scheduling--2] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:23:45.005,线程:scheduling--2
2022-08-28 16:23:46.005  INFO 58093 --- [  scheduling--1] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:23:46.005,线程:scheduling--1
2022-08-28 16:23:48.005  INFO 58093 --- [  scheduling--3] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:23:48.005,线程:scheduling--3
2022-08-28 16:23:50.006  INFO 58093 --- [  scheduling--3] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:23:50.006,线程:scheduling--3
2022-08-28 16:23:50.006  INFO 58093 --- [  scheduling--4] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:23:50.006,线程:scheduling--4
2022-08-28 16:23:52.004  INFO 58093 --- [  scheduling--3] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:23:52.004,线程:scheduling--3
2022-08-28 16:23:54.003  INFO 58093 --- [  scheduling--3] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:23:54.003,线程:scheduling--3
2022-08-28 16:23:55.006  INFO 58093 --- [  scheduling--4] c.e.demo.scheduling.SchedulingTest       : testOne每五秒钟执行一次:时间:2022-08-28T16:23:55.006,线程:scheduling--4
2022-08-28 16:23:56.005  INFO 58093 --- [  scheduling--3] c.e.demo.scheduling.SchedulingTest       : testTwo每秒钟执行一次:时间:2022-08-28T16:23:56.005,线程:scheduling--3

从执行结果可以看出来,两个定时任务之间是互不影响的。自己执行自己的。 但是需要注意的是,如果定时任务执行时间超过了设置的时间,那么在任务执行期间的重复执行的任务是会被舍弃的,不管是否配置了线程池。也就是定时任务只会影响到自己的。

如果是使用的实现SchedulingConfigurer接口的定时任务,上面的配置是不生效的,反而会造成@Scheduled注解的定时任务也只有一个线程执行。 而如果要使实现SchedulingConfigurer接口的定时任务用多线程跑的话,需要在实现SchedulingConfigurer接口的类中手动设置线程池,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Configuration
public class One implements SchedulingConfigurer {
    @Bean(name = "myThreadPoolTaskScheduler")
    public TaskScheduler getMyThreadPoolTaskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(8);
        taskScheduler.setThreadNamePrefix("test-Scheduled-");
        taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //调度器shutdown被调用时等待当前被调度的任务完成
        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        //等待时长 60s
        taskScheduler.setAwaitTerminationSeconds(60);
        return taskScheduler;
    }
    
    private static final Logger log= LoggerFactory.getLogger(One.class);
    @Autowired
    private TaskScheduler myThreadPoolTaskScheduler;
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setTaskScheduler(myThreadPoolTaskScheduler);
        taskRegistrar.addTriggerTask(
                //1.添加任务内容(Runnable)
                () -> {
                    try {
                        log.info("One每五秒钟执行一次:时间:{},线程:{}",LocalDateTime.now(),Thread.currentThread().getName());
                        Thread.sleep(1000*6);
                    }catch (Exception e){
                        log.error("定时任务错误",e);
                    }
                },
                //2.设置执行周期(Trigger)
                triggerContext -> new CronTrigger("*/5 * * * * ?").nextExecutionTime(triggerContext)
        );
    }
}

这里需要注意的是,线程池需要自己定义,这里为了方便,都定义到一个类了。

最后

使用定时任务时:

  1. @EnableScheduling 这样的注解,项目中配置到一个地方就行了,不用每个定时任务类或者配置类都去添加这个注解。
  2. 定时任务尽量使用多线程,不要使用默认配置的单线程。当然甚至还可以使用异步注解来将定时任务变成多线程执行。
坚持原创技术分享,您的支持将鼓励我继续创作!