背景
在项⽬开发过程中,我们经常需要执⾏具有周期性的任务。通过定时任务可以很好的帮助我们实现。我们拿常⽤的⼏种定时任务框架做⼀个⽐较:
从以上表格可以看出,Spring Schedule框架功能完善,简单易⽤。对于中⼩型项⽬需求,Spring Schedule是完全可以胜任的。
1、springboot集成schedule1.1 添加maven依赖包
由于Spring Schedule包含在spring-boot-starter基础模块中了,所有不需要增加额外的依赖。
1.2 启动类,添加启动注解
在springboot⼊⼝或者配置类中增加@EnableScheduling注解即可启⽤定时任务。
@EnableScheduling@SpringBootApplication
public class ScheduleApplication { public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class, args); }}
1.3.添加定时任务
我们将对Spring Schedule三种任务调度器分别举例说明。1.3.1 Cron表达式
类似于Linux下的Cron表达式时间定义规则。Cron表达式由6或7个空格分隔的时间字段组成,如下图:
常⽤表达式:
举个栗⼦:
添加⼀个work()⽅法,每10秒执⾏⼀次。
注意 :当⽅法的执⾏时间超过任务调度频率时,调度器会在下个周期执⾏。
如:假设work()⽅法在第0秒开始执⾏,⽅法执⾏了12秒,那么下⼀次执⾏work()⽅法的时间是第20秒。
@Component
public class MyTask {
@Scheduled(cron = \"0/10 * * * * *\") public void work() { // task execution logic }}
1.3.2 固定间隔任务
下⼀次的任务执⾏时间,是从⽅法最后⼀次任务执⾏结束时间开始计算。并以此规则开始周期性的执⾏任务。举个栗⼦:
添加⼀个work()⽅法,每隔10秒执⾏⼀次。
例如:假设work()⽅法在第0秒开始执⾏,⽅法执⾏了12秒,那么下⼀次执⾏work()⽅法的时间是第22秒。
@Scheduled(fixedDelay = 1000*10)public void work() { // task execution logic}
1.3.3 固定频率任务
按照指定频率执⾏任务,并以此规则开始周期性的执⾏调度。举个栗⼦:
添加⼀个work()⽅法,每10秒执⾏⼀次。
注意 :当⽅法的执⾏时间超过任务调度频率时,调度器会在当前⽅法执⾏完成后⽴即执⾏下次任务。例如:假设work()⽅法在第0秒开始执⾏,⽅法执⾏了12秒,那么下⼀次执⾏work()⽅法的时间是第12秒。
@Scheduled(fixedRate = 1000*10)public void work() { // task execution logic}
2、配置TaskScheduler线程池
在实际项⽬中,我们⼀个系统可能会定义多个定时任务。那么多个定时任务之间是可以相互独⽴且可以并⾏执⾏的。
通过查看org.springframework.scheduling.config.ScheduledTaskRegistrar源代码,发现spring默认会创建⼀个单线程池。这样对于我们的多任务调度可能会是致命的,当多个任务并发(或需要在同⼀时间)执⾏时,任务调度器就会出现时间漂移,任务执⾏时间将不确定。
protected void scheduleTasks() { if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor(); this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor); }
//省略...}
2.1 ⾃定义线程池
新增⼀个配置类,实现SchedulingConfigurer接⼝。重写configureTasks⽅法,通过taskRegistrar设置⾃定义线程池。
@Configuration
public class ScheduleConfig implements SchedulingConfigurer { @Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(taskExecutor()); }
@Bean(destroyMethod=\"shutdown\") public Executor taskExecutor() {
return Executors.newScheduledThreadPool(20); }}
3、实际应⽤中的问题
3.1 Web应⽤中的启动和关闭问题
我们知道通过spring加载或初始化的Bean,在服务停⽌的时候,spring会⾃动卸载(销毁)。但是由于线程是JVM级别的,如果⽤户在Web应⽤中启动了⼀个线程,那么这个线程的⽣命周期并不会和Web应⽤保持⼀致。也就是说,即使Web应⽤停⽌了,这个线程依然没有结束(死亡)。解决⽅法:
1)当前对象是通过spring初始化
spring在卸载(销毁)实例时,会调⽤实例的destroy⽅法。通过实现DisposableBean接⼝覆盖destroy⽅法实现。在destroy⽅法中主动关闭线程。
@Component
public class MyTask implements DisposableBean{ @Override
public void destroy() throws Exception { //关闭线程或线程池
ThreadPoolTaskScheduler scheduler = (ThreadPoolTaskScheduler)applicationContext.getBean(\"scheduler\"); scheduler.shutdown(); }
//省略...}
2)当前对象不是通过spring初始化(管理)
那么我们可以增加⼀个Servlet上下⽂监听器,在Servlet服务停⽌的时候主动关闭线程。
public class MyTaskListenter implements ServletContextListener{ @Override
public void contextDestroyed(ServletContextEvent arg0) { //关闭线程或线程池 }
//省略...}
3.2 分布式部署问题
在实际项⽬中,我们的系统通常会做集群、分布式或灾备部署。那么定时任务就可能出现并发问题,即同⼀个任务在多个服务器上同时在运⾏。解决⽅法(分布式锁):1)通过数据库表锁2)通过缓存中间件3)通过Zookeeper实现总结:
spring schedule给我们提供了⼀套简单、快速、⾼效、稳定的定时任务框架。但需要考虑线程的⽣命周期及分布式部署问题。
以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。
因篇幅问题不能全部显示,请点此查看更多更全内容