SpringBoot之定时任务 | 小册免费学

1,510 阅读5分钟

Spring Boot实现定时任务

只需要在类上加上@EnableScheduling,在方法上加上@Scheduled两个注解即可启用定时任务

Spring Boot提供的定时任务存在的小坑

@EnalbleScheduling

@EnableScheduling可以放在启动类,方便整体把控。

@Scheduled几个属性

不同的属性有不同的功效

fixedDelay距离上一次任务的时间

效果:上一个任务结束后间隔多久执行

@Slf4j
@Component
@EnableScheduling
public class TaskOne {
    @Scheduled(fixedDelay = 2 * 1000)
    public void taskOne(){
        log.info("taskOne Begin...");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("task One End...");
    }
}

下一个任务开始在上一个任务结束后 间隔 fixedDelay 时间执行

fixedRate : 距离上一次任务开始间隔的时间 执行下一次任务开始

@Scheduled(fixedRate = 5 * 1000)

不论上一个任务执行是否大于间隔时间,下一个任务都会在上一个任务开始后的 fixedRate 间隔后执行下一个任务

但spring boot的定时任务默认是单线程的,即使下一个任务从上一个任务开始后等待了超过了fixedRate时间,仍然要等待上一个任务执行完成。

  • fixDelay 上一个任务结束与下一个任务开始的时间间隔
  • fixRate 上一个任务开始与下一个任务开始的时间间隔

如何解决fixedRate,当下一个任务执行等待时间超过了fixedRate时间后会等待上一个任务执行完问题?

为定时任务指定线程池,每个任务都跑在独立的线程中,不存在等待上一个任务的情况 问题变成如何将SpringBoot的定时任务配置为多线程模式呢?

initialDelay:启动多少秒后开始首次任务

fixedDelay、fixedRate都是项目启动后就立即执行,随后按照指定间隔时间重复执行。 推迟首次任务执行时间,用initialDelay指定

@Scheduled(fixedRate = 5 * 1000,initialDelay = 2 * 1000)

cron:定时执行(最常用)

cron表达式,一般写6位即可。分别代表@Schedule(cron = "秒 分 时 日 月 星期[年]"),第7位年可以不写

cron表达式实用规则:

*/number 表示“每隔nubmer”

逗号表示“或”,如12,13,16表示12或13或16

在线cron生成器:www.pppet.net/

一个线上的坑

@Slf4j
@Component
@EnableScheduling
public class TaskOne {
    @Scheduled(cron = "*/10 * * * * ?")
    public void taskOne(){
        log.info("taskOne Begin...");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("taskOne End...");
    }

    @Scheduled(cron = "*/10 * * * * ?")
    public void taskTwo(){
        log.info("taskTwo Begin...");
        try {
            TimeUnit.SECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("taskTwo End...");
    }
}

设定多个定时任务都是同一时刻开启的,当一个定时任务执行时间很长或卡死,会导致另外的定时任务等待,为什么?

Spring Boot定时任务默认时单线程的。

SpringBoot定时任务配置多线程:

/**
 * 线程池配置
 *
 */
@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 ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }

}

然后在定时任务上加上@Async("taskExecutor"),

项目中可能会配置多个线程池,创建线程池时给定一个有意义的名字,是一个好的习惯,专池专用。 但是此时还是会有问题,但一个任务执行完成后,线程会随机去执行一个任务,当走到任务2时,线程就会卡死在任务2中。最终所有线程卡死在任务2

如何保证定时任务可用

当定时任务异常时,光配置定时任务池是不行的,最终线程池仍然会枯竭,导致所有线程阻塞,除非每个定时任务有专门的线程池。

所以发生定时任务耗时异常这种情况时,最重要的是及时发现并修复。

在配置线程池时,可以指定拒绝策略,线程池队列满了之后触发。

如何替换Spring定时任务中的线程池呢?

查看源码上发现SpringBoot对外暴露了三种自定义定时任务池的方法:

  • 如果存在SchedulingConfigurer类型的Bean,用SchedulingConfigurer
  • 如果存在TaskSheduler类型的Bean,用TaskScheduler
  • 如果存在ScheduledExecutorService的Bean,用ScheduledExecutorService
  • 以上都没有用默认的 上面的任意一种定时任务线程池都会调用ScheduledTaskRegistrar#setScheduler为taskScheduler赋值。

实际开发中建议使用自定义的拒绝策略,为什么?

实际开发中发现线程池不够用了,能直接跑主线程吗,那可能主线程也被阻塞,能直接丢弃吗? 不确定是否对业务没有影响,如果业务本身不在乎请求失败,那就没关系,否则不适合使用丢弃策略。

一般可以选择在自定义丢弃策略中使用消息队列(延后缓冲),或者发邮箱告警(及时发现),拒绝策略只要实现RejectedExecutionHandler接口即可:

@Configuration
@EnableAsync
@Slf4j
public class ThreadPoolTaskConfig {

    @Bean("executor")
    public Executor getExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(3);
        executor.setMaxPoolSize(3);
        executor.setQueueCapacity(5);
        executor.setKeepAliveSeconds(10);
        executor.setThreadNamePrefix("async-task-");

        // 线程池对拒绝任务的处理策略
        executor.setRejectedExecutionHandler((r, executor1) -> {
            log.info("发送告警邮件======>:嘿沙雕,线上定时任务卡爆了, 当前线程名称为:{}, 当前线程池队列长度为:{}",
                    r.toString(),
                    executor1.getQueue().size());
        });
        // 初始化
        executor.initialize();
        return executor;
    }
}

这样就具备了完善的告警机制了,可以及时发现线上的问题了。

当项目是多节点部署时,就需要考虑不同节点重复执行的问题。这时就需要考虑使用分布式锁了,但是使用分布式锁,有需要考虑是否会产生死锁。

Spring Boot定时任务的不足:

  • 不支持集群时单节点启动(可能会产生不同节点重复执行同一定时任务)
  • 不支持分片任务
  • 不支持失败重试(一个任务失败了就失败了,不会重试)
  • 动态调整比较繁琐(配置动态配置任务启动的时间点很复杂)

建议:在多节点部署,分布式项目中,使用Elastic-job或者XXL-Job

上面线程池严格意义上来说不是定时任务线程池,而是异步任务线程池,定时任务理论上只要加@Scheduled,不需要特意指定@Async,@Async是异步线程相关的。

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情