springboot @Schedule定時任務你會用嗎

springboot @Schedule定時任務

任務間不允許併發且不允許同任務交疊

使用springboot 定時任務很簡單,只需在啓動類或者配置類上添加@EnableScheduling,然後在需要定時執行的類上添加@Conponent註解,最後在需要定時執行的方法上添加@Schedule註解並指定執行方式即可:

啓動類:

@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class, args);
    }
}

任務類:

@Component
public class ScheduleTask {
	/**
	 * cron任務
	 */
	@Scheduled(cron = "0/10 * * * * ?")
	public void task1() {
		System.out.println("task1");
	}

	/**
	 * fixed rate任務
	 */
	@Scheduled(fixedRate = 20000)
	public void task2() {
		System.out.println("task2");
	}
}

這樣可以實現簡單的定時任務執行,特別是整個項目只有一個定時任務的時候非常適用,因爲不存在任務間的併發,不用考慮線程池。

默認情況下,spring scheduler是單線程的,即任務間默認不併發;並且前後時間點的任務不交迭(overlap),即同一個定時任務,本次任務執行時間到了,上一次任務還未執行完成,那麼本次任務直接放棄,熟悉quartz的應該知道quartz可以在job類上使用@DisallowConcurrentExecution實現類似的功能。

也就是說,默認情況下,springboot scheduler禁止任務間和相同任務的併發。
一下兩個例子可以證明:

@Component
@Slf4j
public class ScheduleTask {

    @Scheduled(cron = "0/10 * * * * ?")
    public void task1() throws InterruptedException {
        log.info("task1");
        Thread.sleep(5000L);
    }

    @Scheduled(cron = "0/10 * * * * ?")
    public void task2() throws InterruptedException {
        log.info("task2");
        Thread.sleep(5000L);
    }
}
    /**
     * 2020-03-24 15:42:00.005  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 15:42:05.008  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 15:42:10.012  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 15:42:20.002  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 15:42:25.005  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 15:42:30.009  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 15:42:40.004  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 15:42:45.010  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 15:42:50.017  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 15:43:00.008  INFO 3611 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     */

根據控制檯輸出判斷:

  • 兩個任務的執行頻率是一樣的,cron表達式都是一樣的,但是在同一個時間點並沒有兩個任務同時執行的日誌;
  • 其中任務依然是在線程池中執行的,但線程池中最多隻有一個活躍線程[pool-1-thread-1],也就相當於是單線程。

因此可以得到默認情況下任務間是無法併發的結論,可以參考源碼得到印證。

@Component
@Slf4j
public class ScheduleTask {

    @Scheduled(cron = "0/10 * * * * ?")
    public void task1() throws InterruptedException {
        log.info("task1");
        Thread.sleep(11000L);
    }
    /**
     * 2020-03-24 15:52:50.002  INFO 3659 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 15:53:10.001  INFO 3659 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 15:53:30.003  INFO 3659 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     */

}

對於單個任務,如果任務之間的間隔小於任務執行時間,那麼當任務準備執行時發現前一次任務還在執行就會主動放棄本次任務。這一點可以從上面的例子輸出可以判斷出來:10s執行一次的任務,因爲執行時間時11s,硬生生變成了20s執行一次。

任務間允許併發且允許同任務交疊

前面“任務間不併發且同任務執行不交疊”是解決併發最保守的方案,那這個“任務間允許併發且允許同任務交疊”就是最開放的解決方案:
首先在啓動類或者配置類添加@EnableAsync註解

@SpringBootApplication
@EnableScheduling
@EnableAsync
public class ScheduleApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class, args);
    }

}

然後在需要併發的方法或類上添加@Async註解

@Component
@Slf4j
@Async
public class ScheduleTask {

    @Scheduled(cron = "0/10 * * * * ?")
    public void task1() throws InterruptedException {
        log.info("task1");
        Thread.sleep(11000L);
    }

    @Scheduled(cron = "0/10 * * * * ?")
    public void task2() throws InterruptedException {
        log.info("task2");
        Thread.sleep(11000L);
    }
    /**
     * 2020-03-24 16:01:40.015  INFO 3786 --- [cTaskExecutor-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 16:01:40.015  INFO 3786 --- [cTaskExecutor-2] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:01:50.003  INFO 3786 --- [cTaskExecutor-3] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:01:50.004  INFO 3786 --- [cTaskExecutor-4] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 16:02:00.004  INFO 3786 --- [cTaskExecutor-5] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 16:02:00.004  INFO 3786 --- [cTaskExecutor-6] g.c.spring.boot.schedule.ScheduleTask    : task1
     */
}

可以看到兩個任務同時開始,並且上一個任務未執行完成,後一個任務依然準時開始執行。

任務間允許併發且允許同任務交疊和不交迭同時存在

基於上面的例子,如果只在某一個任務添加@Async註解:

@Component
@Slf4j
public class ScheduleTask {

    @Scheduled(cron = "0/10 * * * * ?")
    @Async
    public void task1() throws InterruptedException {
        log.info("task1");
        Thread.sleep(11000L);
    }

    @Scheduled(cron = "0/10 * * * * ?")
    public void task2() throws InterruptedException {
        log.info("task2");
        Thread.sleep(15000L);
    }

    /**
     * 2020-03-24 16:12:40.009  INFO 3851 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 16:12:55.024  INFO 3851 --- [cTaskExecutor-1] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:13:00.005  INFO 3851 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 16:13:00.005  INFO 3851 --- [cTaskExecutor-2] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:13:15.011  INFO 3851 --- [cTaskExecutor-3] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:13:20.002  INFO 3851 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 16:13:35.006  INFO 3851 --- [cTaskExecutor-4] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:13:40.004  INFO 3851 --- [pool-1-thread-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 16:13:40.004  INFO 3851 --- [cTaskExecutor-5] g.c.spring.boot.schedule.ScheduleTask    : task1
     */

}

可以看到

  • 對於同一個任務,只有添加了@Async註解的任務纔會交疊執行,而未添加註解的任務依然不會交疊;
  • 對於不同任務之間,雖然整體上是併發的,但是沒有添加@Async註解的任務會影響任務間的併發:同一個觸發時間點,只有當未添加@Async註解的任務執行完成後,纔會將其他Async任務添加到線程池中。
    發現控制檯打印的線程名後綴是一個無限增長的數字([cTaskExecutor-%d]),推斷應該默認創建了一個CachedThreadPool,這個線程池其實可以自己創建和配置,避免可能的OOM,控制檯打印的信息也印證了這一點:
2020-03-24 16:12:55.021  INFO 3851 --- [pool-1-thread-1] .s.a.AnnotationAsyncExecutionInterceptor : No task executor bean found for async processing: no bean of type TaskExecutor and no bean named 'taskExecutor' either

添加如下配置類:

@Configuration
public class TaskExecutorConfigure implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setTaskScheduler(taskScheduler());
    }

    @Bean(destroyMethod = "shutdown", name = "taskScheduler")
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("task-pool-");
        scheduler.setAwaitTerminationSeconds(600);
        scheduler.setWaitForTasksToCompleteOnShutdown(false);
        return scheduler;
    }
}

控制檯輸出如下:

2020-03-24 16:33:00.013  INFO 4044 --- [    task-pool-1] g.c.spring.boot.schedule.ScheduleTask    : task2
2020-03-24 16:33:00.013  INFO 4044 --- [    task-pool-3] g.c.spring.boot.schedule.ScheduleTask    : task1
2020-03-24 16:33:10.002  INFO 4044 --- [    task-pool-4] g.c.spring.boot.schedule.ScheduleTask    : task1
2020-03-24 16:33:20.001  INFO 4044 --- [    task-pool-2] g.c.spring.boot.schedule.ScheduleTask    : task2
2020-03-24 16:33:20.001  INFO 4044 --- [    task-pool-6] g.c.spring.boot.schedule.ScheduleTask    : task1
2020-03-24 16:33:30.001  INFO 4044 --- [    task-pool-1] g.c.spring.boot.schedule.ScheduleTask    : task1
2020-03-24 16:33:40.004  INFO 4044 --- [    task-pool-5] g.c.spring.boot.schedule.ScheduleTask    : task1

可以看到任務間已經開始真正的併發,而允許交疊和不允許交疊的任務依然各自遵守自己的規則。

任務間允許併發但不允許同任務交疊

其實這裏跟最保守的方式只有一個區別:使用一個允許更多活躍線程的線程池,而非默認的但一線程池。這裏不能使用@Async註解,否則無法保證同任務不交迭。

配置類:

@Configuration
public class TaskExecutorConfigure implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setTaskScheduler(taskScheduler());
    }

    @Bean(destroyMethod = "shutdown", name = "taskScheduler")
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("task-pool-");
        scheduler.setAwaitTerminationSeconds(600);
        scheduler.setWaitForTasksToCompleteOnShutdown(false);
        return scheduler;
    }
}

任務類

@Component
@Slf4j
public class ScheduleTask {

    @Scheduled(cron = "0/10 * * * * ?")
    public void task1() throws InterruptedException {
        log.info("task1");
        Thread.sleep(1000L);
    }

    @Scheduled(cron = "0/10 * * * * ?")
    public void task2() throws InterruptedException {
        log.info("task2");
        Thread.sleep(15000L);
    }

    /**
     * 2020-03-24 16:39:30.002  INFO 4106 --- [    task-pool-2] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:39:30.002  INFO 4106 --- [    task-pool-1] g.c.spring.boot.schedule.ScheduleTask    : task2
     * 2020-03-24 16:39:40.002  INFO 4106 --- [    task-pool-2] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:39:50.002  INFO 4106 --- [    task-pool-2] g.c.spring.boot.schedule.ScheduleTask    : task1
     * 2020-03-24 16:39:50.002  INFO 4106 --- [    task-pool-3] g.c.spring.boot.schedule.ScheduleTask    : task2
     */
}

可以看到任務間併發無誤,同任務不允許交疊。因爲任務間不允許交疊,所以同一時間,系統中存在的任務數就是所有定時任務書,因此只需要設置線程池的容量剛好是定時任務數即可,這也是大部分定時任務的真實需求,也能保證不OOM。

spring cloud多實例任務不併發

如果使用了spring cloud或者其他高可用分佈式部署方案,一般來說定時任務最好只在某一個實例上執行即可,這時候,sprint boot如何保證定時任務不在多個實例上執行呢?解決方案思路有:

  • 分佈式鎖:如果有分佈式鎖,比如用Redis實現的分佈式鎖,使用實例搶佔的方式,誰搶到了就誰執行,沒有搶到的主動放棄,還可以實現負載均衡;
  • 使用數據庫鎖:熟悉quartz集羣的應該知道,quartz集羣保證任務不在多個實例執行的方式是藉助MySQL的行鎖(select for update),這種方式實現簡單,也能實現負載均衡,和分佈式鎖有異曲同工之妙;
  • 藉助spring cloud實例列表:每個節點從eureka獲取所有註冊實例,然後各個節點根據一致性算法計算出應該執行該任務的節點(保證每個節點計算的結果一致),比如選取最小的一個節點或最大的一個節點,在判斷自己是不是該節點,如果是就執行,否則就放棄。這種方法雖然實現簡單,但是無法做到負載均衡,除非給任務添加標識(特定任務有現在特定實例執行),或者用分時間段的方式(特定時間段的任務在特定實例上執行)等來實現負載。

基於第三種方式,有一個簡單的實現:

@Slf4j
public abstract class AbstractSchedulingTask {
    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private Environment environment;

    protected boolean canExecute() {
        List<ServiceInstance> instances = discoveryClient.getInstances(environment.getProperty("spring.application.name"));
        log.info("實例列表:{}", JSON.toJSONString(instances));

        // 如果沒有任何一個實例組冊到Eureka,不執行任務
        if(instances == null || instances.isEmpty()) {
            return false;
        }

        String currentInstanceId = environment.getProperty("eureka.instance.instance-id");
        log.info("當前實例:{}", currentInstanceId);

        // 所有註冊到Eureka中的實例按照ID升序排列
        instances = instances.stream().sorted(Comparator.comparing(ServiceInstance::getInstanceId)).collect(Collectors.toList());

        return instances.get(0).getInstanceId().equalsIgnoreCase(currentInstanceId);
    }
}

總結

  • 任務間不允許併發且不允許同任務交疊:單個任務或任務執行時間很短
  • 任務間允許併發且允許同任務交疊:多個任務且要求每個時間點都必須執行,但要注意控制線程池,否則容易OOM
  • 任務間允許併發且允許同任務交疊和不交迭同時存在:滿足複雜場景
  • 任務間允許併發但不允許同任務交疊:滿足大部分場景
  • spring cloud多實例任務不併發:多種實現方式
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章