栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java > SpringBoot

springboot之一文带你搞懂Scheduler定时器(修订-详尽版)

SpringBoot 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

springboot之一文带你搞懂Scheduler定时器(修订-详尽版)

本篇文章是SpringBoot项目实战(6):开启定时任务一文的修订版。

在开发程序中总是避免不了一些周期性定时任务,比如定时同步数据库、定时发送邮件、定时初始化数据等等。

那么在springboot中如何使用定时任务呢? 本文将由浅入深逐渐揭开定时任务的“神秘面纱”。

本文知识点:

  1. springboot中如何使用Scheduler
  2. springboot中如何动态修改Scheduler的执行时间(cron)
  3. springboot中如何实现多线程并行任务

想了解springboot中的Scheduler?看懂本文妥妥的了~!

如何使用Scheduler?
  1. 使用@EnableScheduling启用定时任务
  2. 使用@Scheduled编写相关定时任务

开启定时任务

在程序中添加@EnableScheduling注解即可启用Spring的定时任务功能,这类似于Spring的XML中的功能。

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

关于@Scheduled

通过查看Scheduled注解类的源码可知该注解支持如下几种方式使用定时任务

  1. cron()
  2. fixedDelay()
  3. fixedDelayString()
  4. fixedRate()
  5. fixedRateString()
  6. initialDelay()
  7. initialDelayString()

注:由于xx和xxString方法只是参数类型不同用法一致,所以本文对xxString表达式方法不做解释。感兴趣的同学可以去查阅下ScheduledAnnotationBeanPostProcessor类的processScheduled方法。

cron

这个也是定时任务中最常用的一种调度方式,主要在于它的功能强大,使用方便。
支持cron语法,与linux中的crontab表达式类似,不过Java中的cron支持到了秒。
表达式规则如下:

{秒} {分} {时} {日} {月} {周} {年(可选)}

这儿有很多朋友记不住,或者和crontab的表达式记混。

Tips:linux的crontab表达式为:{分} {时} {日} {月} {周}

其实这儿也没有很好的记忆方式,采用最简单的方式:死记硬背即可(( ╯╰ ))。

cron各选项的取值范围及解释:

取值范围 null 备注
0-59 参考①
0-59 参考①
0-23 参考①
1-31 参考②
1-12或JAN-DEC 参考①
1-7或SUN-SAT 参考③
1970-2099 参考①

参考①

“*” 每隔1单位时间触发;
"," 在指定单位时间触发,比如"10,20"代表10单位时间、20单位时间时触发任务
"-" 在指定的范围内触发,比如"5-30"代表从5单位时间到30单位时间时每隔1单位时间触发一次
"/" 触发步进(step),"/“前面的值代表初始值(”“等同"0”),后面的值代表偏移量,比如"0/25"或者"/25"代表从0单位时间开始,每隔25单位时间触发1次;"10-45/20"代表在[10-45]单位时间内每隔20单位时间触发一次

参考②

“* , - /” 这四个用法同①
"?" 与{周}互斥,即意味着若明确指定{周}触发,则表示{日}无意义,以免引起 冲突和混乱;
“L” 如果{日}占位符如果是"L",即意味着当月的最后一天触发
"W "意味着在本月内离当天最近的工作日触发,所谓最近工作日,即当天到工作日的前后最短距离,如果当天即为工作日,则距离为0;所谓本月内的说法,就是不能跨月取到最近工作日,即使前/后月份的最后一天/第一天确实满足最近工作日;因此,"LW"则意味着本月的最后一个工作日触发,"W"强烈依赖{月}
“C” 根据日历触发,由于使用较少,暂时不做解释

参考③

“* , - /” 这四个用法同①
"?" 与{日}互斥,即意味着若明确指定{日}触发,则表示{周}无意义,以免引起冲突和混乱
"L" 如果{周}占位符如果是"L",即意味着星期的的最后一天触发,即星期六触发,L= 7或者 L = SAT,因此,“5L"意味着一个月的最后一个星期四触发
”#" 用来指定具体的周数,"#“前面代表星期,”#"后面代表本月第几周,比如"2#2"表示本月第二周的星期一,"5#3"表示本月第三周的星期四,因此,“5L"这种形式只不过是”#“的特殊形式而已
"C” 根据日历触发,由于使用较少,暂时不做解释

其他关于cron的详细介绍请参考Spring Task 中cron表达式整理记录

@Scheduled(cron = "0/30 7-8 22 7 11 ? ")
public void doJobByCron() throws InterruptedException {
	int index = integer.incrementAndGet();
	System.out.println(String.format("[%s] %s doJobByCron start @ %s", index, Thread.currentThread(), LocalTime.now()));
	// 这儿随机睡几秒,方便查看执行效果
	TimeUnit.SECONDS.sleep(new Random().nextInt(5));
	System.out.println(String.format("[%s] %s doJobByCron end   @ %s", index, Thread.currentThread(), LocalTime.now()));
}

查看打印的结果

[1] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:07:00.004
[1] Thread[pool-1-thread-1,5,main] doJobByCron end   @ 22:07:01.004
[2] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:07:30.001
[2] Thread[pool-1-thread-1,5,main] doJobByCron end   @ 22:07:30.001
[3] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:08:00.001
[3] Thread[pool-1-thread-1,5,main] doJobByCron end   @ 22:08:01.002
[4] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:08:30.002
[4] Thread[pool-1-thread-1,5,main] doJobByCron end   @ 22:08:33.002

给大家安利一款在线生成Cron表达式的神器:http://cron.qqe2.com。使用简单,一看就会。

fixedDelay

上一次任务执行完成后,延时固定长度时间执行下一次任务。关键词:上一次任务执行完成后
就如官方文档中所说:在最后一次调用结束到下一次调用开始之间以毫秒为单位进行等待,等待完成后执行下一次任务

Execute the annotated method with a fixed period in milliseconds between the end of the last invocation and the start of the next.

通过代码可以更好的理解

private AtomicInteger integer = new AtomicInteger(0);

@Scheduled(fixedDelay = 3000)
public void doJobByFixedDelay() throws InterruptedException {
	int index = integer.incrementAndGet();
	System.out.println(String.format("[%s] %s doJobByFixedDelay start @ %s", index, Thread.currentThread(), LocalTime.now()));
	// 这儿随机睡几秒,方便查看执行效果
	TimeUnit.SECONDS.sleep(new Random().nextInt(10));
	System.out.println(String.format("[%s] %s doJobByFixedDelay end   @ %s", index, Thread.currentThread(), LocalTime.now()));
}

查看控制台输出

[1] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:03.235
[1] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end   @ 18:28:06.236
[2] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:09.238
[2] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end   @ 18:28:10.238
[3] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:13.240
[3] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end   @ 18:28:21.240

可以看到不管每个任务实际需要执行多长时间,该任务再次执行的时机永远都是在上一次任务执行完后,延时固定长度时间后执行下一次任务。

fixedRate

官方文档中已经很明白的说明了此方法的用法。

Execute the annotated method with a fixed period in milliseconds between invocations.

按照固定的速率执行任务,无论之前的任务是否执行完毕。关键词:不管前面的任务是否执行完毕

private AtomicInteger integer = new AtomicInteger(0);

@Scheduled(fixedRate = 3000)
public void doJobByFixedRate() throws InterruptedException {
	int index = integer.incrementAndGet();
	System.out.println(String.format("[%s] %s doJobByFixedRate start @ %s", index, Thread.currentThread(), LocalTime.now()));
	// 这儿随机睡几秒,方便查看执行效果
	TimeUnit.SECONDS.sleep(new Random().nextInt(10));
	System.out.println(String.format("[%s] %s doJobByFixedRate end   @ %s", index, Thread.currentThread(), LocalTime.now()));
}

查看控制台打印的结果

[1] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:23.520
[1] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:24.521
[2] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:26.515
[2] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:31.516
[3] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:31.516
[3] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:31.516
[4] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:32.515
[4] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:39.516
[5] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:39.516
[5] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:40.517
[6] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:40.517
[6] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:41.517
[7] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:41.517
[7] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:47.519

这儿需要重点关注每个任务的起始时间下一次任务的起始时间。如果说我们不看这个结果,直接就代码来说,我们会认为,程序每隔3秒执行一次,第二次任务和第一次任务的间隔时间是3秒,没毛病。但是我们通过上面打印的日志发现,程序并不是按照我们预想的流程执行的。

注:为方便阅读,任务命名为T+任务的编号,比如T1表示第一个任务,以此类推。

从上面结果看,T1起始时间到T2起始时间之间间隔了3秒,T2起始时间到T3起始时间间隔了5秒,T3起始时间到T4起始时间间隔了1秒。

为什么会有这种现象呢?这正如上面所说:“按照固定的延迟时间执行任务,无论之前的任务是否执行完毕

那么按照这种规则,咱们预先估计下每次任务的执行时间就能解释上面的问题。T1起始时间是23秒,T2起始时间是26,以此类推,T3理论上是29,T4是32,T5是35,T6是38,T7是41…

这种机制也可以称为“任务编排”,也就是说从fixedRate任务开始的那一刻,它后续的任务执行的时间已经被预先编排好了。如果定时任务执行的时间 大于fixedRate指定的延迟时间,则定时任务会在上一个任务结束后立即执行;如果定时任务执行的时间 小于fixedRate指定的延迟时间,则定时任务会在上一个任务结束后等到预编排的时间时执行。

initialDelay

该条指令主要是用于配合fixedRate和fixedDelay使用的,作用是在fixedRate或fixedDelay任务第一次执行之前要延迟的毫秒数,说白了:它的作用就是在程序启动后,并不会立即执行该定时任务,它将在延迟一段时间后才会执行该定时任务

Number of milliseconds to delay before the first execution of a fixedRate() or fixedDelay() task.

本例为了方便测试,需要首先知道这个定时任务的bean在什么时候装载完成,或者说这个定时任务的bean什么时候初始化完成,所以,需要处理一下代码。

@Component
public class AppSchedulingConfigurer implements ApplicationListener {

    private AtomicInteger integer = new AtomicInteger(0);


    
    @Scheduled(initialDelay = 5000, fixedRate = 3000)
    public void doJobByInitialDelay() throws InterruptedException {
 int index = integer.incrementAndGet();
 System.out.println(String.format("[%s] %s doJobByInitialDelay start @ %s", index, Thread.currentThread(), LocalTime.now()));
 // 这儿随机睡几秒,方便查看执行效果
 TimeUnit.SECONDS.sleep(new Random().nextInt(5));
 System.out.println(String.format("[%s] %s doJobByInitialDelay end   @ %s", index, Thread.currentThread(), LocalTime.now()));
    }

    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
 System.out.println(String.format("springboot上下文context准备完毕时. %s start @ %s", Thread.currentThread(), LocalTime.now()));
    }
}

如上,使用Spring的事件监听应用启动的时间,以此打印出initialDelay应该从什么时候开始等待。

(关于Spring和Springboot的事件,本文不做深入介绍,后期会专文讲解这一块内容)

查看打印结果

springboot上下文context准备完毕时. Thread[main,5,main] start @ 21:18:42.092
[1] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:47.023
[1] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:18:51.024
[2] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:51.024
[2] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:18:53.025
[3] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:53.025
[3] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:18:57.025
[4] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:57.025
[4] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:18:57.025
[5] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:59.025
[5] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:19:00.025

由此可以明显看出,initialDelay确实是在bean上下文准备完毕(容器已初始化完成)时延迟了5秒钟后执行的fixedRate任务。

如何动态修改执行时间(cron)?

实现动态配置cron的好处就是可以随时修改任务的执行表达式而不用重启服务。

想实现这种功能,我们只需要实现SchedulingConfigurer接口重写configureTasks接口即可。

@Component
public class DynamicScheduledConfigurer implements SchedulingConfigurer {

    // 默认每秒执行一次定时任务
    private String cron = "0/1 * * * * ?";
    private AtomicInteger integer = new AtomicInteger(0);

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
 taskRegistrar.addTriggerTask(() -> {
     int index = integer.incrementAndGet();
     System.out.println(String.format("[%s] %s 动态定时任务 start @ %s", index, Thread.currentThread(), LocalTime.now()));
 }, triggerContext -> {
     CronTrigger trigger = new CronTrigger(this.getCron());
     return trigger.nextExecutionTime(triggerContext);
 });
    }

    public String getCron() {
 return cron;
    }

    public void setCron(String cron) {
 System.out.println(String.format("%s Cron已修改!修改前:%s,修改后:%s @ %s", Thread.currentThread(), this.cron, cron, LocalTime.now()));
 this.cron = cron;
    }
}

例子中默认的cron表示每秒执行一次定时任务,我们要做的就是动态修改这个执行时间。

接下来编写一个controller负责处理cron以及预编排5组该cron将要执行的时间节点

@SpringBootApplication
@Controller
@EnableScheduling
public class ScheduleApplaction {

    @Autowired
    DynamicScheduledConfigurer dynamicScheduledConfigurer;

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

    @RequestMapping("/")
    public String index() {
 return "index";
    }

    
    @RequestMapping("/updateTask")
    @ResponseBody
    public void updateTask(String cron) {
 dynamicScheduledConfigurer.setCron(cron);
    }

    
    @RequestMapping("/parseCron")
    @ResponseBody
    public List parseCron(String cron) throws IOException {
 String urlNameString = "http://cron.qqe2.com/CalcRunTime.ashx?Cronexpression=" + URLEncoder.encode(cron, "UTF-8");
 URL realUrl = new URL(urlNameString);
 URLConnection connection = realUrl.openConnection();
 connection.setRequestProperty("accept", "*/*");
 connection.setRequestProperty("connection", "Keep-Alive");
 connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");
 connection.connect();

 StringBuilder result = new StringBuilder();
 try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
     String line;
     while ((line = in.readLine()) != null) {
  result.append(line);
     }
 } catch (Exception e) {
     e.printStackTrace();
 }
 return JSONArray.parseArray(result.toString(), String.class);
    }
}

html页面




    
    动态修改cron




    
    

    
最近5次运行的时间

启动项目后先查看控制台输出,默认应该是每秒执行一次定时任务。

[1] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:23.010
[2] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:24.001
[3] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:25.001
[4] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:26.002

接下来修改一下默认cron,如下图,将定时任务修改为每5秒执行一次

[56] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:18.002
[57] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:19.001
Thread[http-nio-8080-exec-7,5,main] Cron已修改!修改前:0/1 * * * * ?,修改后:0/5 * * * * ? @ 14:01:19.963
[58] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:20.001
[59] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:25.002
[60] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:30.002
[61] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:35.002
[62] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:40.001
[63] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:45.002

ok,到此为止就实现了动态修改定时任务的功能。

如何实现多线程并行任务

通过上面几个例子,可能细心的朋友已经发现了一个规律:上面所有的定时任务都是单线程的。

其实也正式如此,如官方文档中解释的:

By default, will be searching for an associated scheduler definition: either a unique TaskScheduler bean in the context, or a TaskScheduler bean named “taskScheduler” otherwise; the same lookup will also be performed for a ScheduledExecutorService bean. If neither of the two is resolvable, a local single-threaded default scheduler will be created and used within the registrar.

大概意思就是:默认情况下会检索是否指定了一个自定义的TaskScheduler,如果没有的情况下,会创建并使用一个本地单线程的任务调度器(线程池)。这一点,可以在ScheduledTaskRegistrar类(定时任务注册类)中加以佐证:

public void afterPropertiesSet() {
	this.scheduleTasks();
}

protected void scheduleTasks() {
	if (this.taskScheduler == null) {
		this.localExecutor = Executors.newSingleThreadScheduledExecutor();
		this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
	}
	// ...
}

那么我们如何将调度器改造成多线程形式的呢?继续向下看文档

When more control is desired, a @Configuration class may implement SchedulingConfigurer. This allows access to the underlying ScheduledTaskRegistrar instance.

意思很明了,我们可以通过实现SchedulingConfigurer接口,然后通过ScheduledTaskRegistrar类去注册自定义的线程池。在SchedulingConfigurer类中已经提供了一个setScheduler方法用来注册自定义的Scheduler Bean

public void setScheduler(Object scheduler) {
	Assert.notNull(scheduler, "Scheduler object must not be null");
	if (scheduler instanceof TaskScheduler) {
		this.taskScheduler = (TaskScheduler)scheduler;
	} else {
		if (!(scheduler instanceof ScheduledExecutorService)) {
			throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
		}

		this.taskScheduler = new ConcurrentTaskScheduler((ScheduledExecutorService)scheduler);
	}

}

接下来我们就按照这种方式扩展一个SchedulingConfigurer,代码如下:

@Component
public class MultiThreadSchedulingConfigurer implements SchedulingConfigurer {

    private AtomicInteger integer = new AtomicInteger(0);

    @Scheduled(cron = "0/1 * * * * ?")
    public void multiThread() {
 System.out.println(String.format("[1] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
    }

    @Scheduled(cron = "0/1 * * * * ?")
    public void multiThread2() {
 System.out.println(String.format("[2] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
    }

    @Scheduled(cron = "0/1 * * * * ?")
    public void multiThread3() {
 System.out.println(String.format("[3] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
 scheduledTaskRegistrar.setScheduler(newExecutors());
    }

    @Bean(destroyMethod = "shutdown")
    private Executor newExecutors() {
 return Executors.newScheduledThreadPool(10, r -> new Thread(r, String.format("ZHYD-Task-%s", integer.incrementAndGet())));
    }
}

如代码所示,我们自定义一个容量为10得线程池,并且自定义一下线程池中线程得命名格式,同时为了方便查看,我们同时开启三个定时任务,在同样的时间同时执行,以此来观察定时器在并行执行时的具体线程分配情况

注意在上面的例子中自定义线程池时,使用了@Bean注解的(destroyMethod=“shutdown”)属性,这个属性可确保在Spring应用程序上下文本身关闭时正确关闭定时任务。

Note in the example above the use of @Bean(destroyMethod=“shutdown”). This ensures that the task executor is properly shut down when the Spring application context itself is closed.

[2] ZHYD-Task-2 exec @ 15:44:14.008
[1] ZHYD-Task-3 exec @ 15:44:14.008
[3] ZHYD-Task-1 exec @ 15:44:14.008
[2] ZHYD-Task-1 exec @ 15:44:15
[1] ZHYD-Task-3 exec @ 15:44:15
[3] ZHYD-Task-2 exec @ 15:44:15
[1] ZHYD-Task-4 exec @ 15:44:16.001
[3] ZHYD-Task-6 exec @ 15:44:16.001
[2] ZHYD-Task-5 exec @ 15:44:16.001
[1] ZHYD-Task-1 exec @ 15:44:17.002
[2] ZHYD-Task-7 exec @ 15:44:17.002
[3] ZHYD-Task-2 exec @ 15:44:17.002
[1] ZHYD-Task-1 exec @ 15:44:18.001

通过上面的结果可以看出,每个定时任务的执行线程都不在是固定的了。

总结
  1. springboot中通过@EnableScheduling注解开启定时任务,@Scheduled支持cron、fixedDelay、fixedRate、initialDelay四种定时任务的表达式
  2. springboot中通过实现SchedulingConfigurer接口并且重写configureTasks方法实现动态配置cron和多线程并行任务

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/234508.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号