前言
實(shí)際開(kāi)發(fā)中,經(jīng)常會(huì)碰到“定期定時(shí)去做一些重復(fù)操作”的需求,這個(gè)時(shí)候,定時(shí)任務(wù)顯得是那么的方便.本章,我們來(lái)講講SpringBoot的定時(shí)任務(wù)如何使用.
分類
使用SpringBoot創(chuàng)建定時(shí)任務(wù)非常簡(jiǎn)單胶哲,目前主要有以下三種創(chuàng)建方式
- 基于注解(@Scheduled)
- 基于接口(SchedulingConfigurer)
- 基于注解多線程定時(shí)任務(wù)
基于注解(單線程)
基于注解@Scheduled默認(rèn)為單線程宪拥,開(kāi)啟多個(gè)任務(wù)時(shí)梭依,任務(wù)的執(zhí)行時(shí)機(jī)會(huì)受上一個(gè)任務(wù)執(zhí)行時(shí)間的影響蒜危。
什么意思呢?
意思就是假設(shè)基于注解注冊(cè)了兩個(gè)定時(shí)任務(wù),我們叫做task1和task2
假設(shè)task1每5s執(zhí)行一次并延時(shí)10s在執(zhí)行下一次.
假設(shè)task2每5s執(zhí)行一次.
@Scheduled默認(rèn)為單線程的執(zhí)行機(jī)制會(huì)導(dǎo)致如下情況:
task2在task1執(zhí)行后才執(zhí)行的(也就是task2實(shí)際上10s后才開(kāi)始執(zhí)行)
@Scheduled支持多種注解(參數(shù)),我們接下來(lái)一一演示
占位符
占位符概念
這邊補(bǔ)上一個(gè)占位符的概念,因?yàn)楹竺鏁?huì)用到.
假設(shè)你將定時(shí)任務(wù)的時(shí)間周期寫(xiě)在了配置文件里,那么此時(shí),你編寫(xiě)定時(shí)任務(wù)時(shí)如何自動(dòng)識(shí)別呢?
這就使用到了占位符.
占位符的使用
yml配置文件中有配置:
time:
cron: */5 * * * * *
interval: 5
此時(shí)使用注解定時(shí)任務(wù)應(yīng)該使用占位符
@Scheduled(cron="${time.cron}")
void testCron1() {
System.out.println("占位符演示1" + System.currentTimeMillis());
}
@Scheduled(cron="*/${time.interval} * * * * *")
void testCron2() {
System.out.println("占位符演示2" + System.currentTimeMillis());
}
八大參數(shù)-cron表達(dá)式(支持占位符)
語(yǔ)法
cron表達(dá)式語(yǔ)法
[秒] [分] [小時(shí)] [日] [月] [周] [年]
注:[年]不是必須的域,可以省略[年]身冀,則一共6個(gè)域
通配符
關(guān)于各個(gè)通配符的意義如下:
- “*”表示所有值钝尸。
例如:在分的字段上設(shè)置 *,表示每一分鐘都會(huì)觸發(fā)。
- “?”表示不指定值搂根。
使用的場(chǎng)景為不需要關(guān)心當(dāng)前設(shè)置這個(gè)字段的值珍促。例如:要在每月
的10號(hào)觸發(fā)一個(gè)操作,但不關(guān)心是周幾剩愧,所以需要周位置的那個(gè)
字段設(shè)置為”?” 具體設(shè)置為 0 0 0 10 * ?
- “-”表示區(qū)間猪叙。
例如 在小時(shí)上設(shè)置 “10-12”,表示 10,11,12點(diǎn)都會(huì)觸發(fā)。
- “,” 表示指定多個(gè)值.
例如在周字段上設(shè)置 “MON,WED,FRI” 表示周一仁卷,周三和周五觸
發(fā)
- “/” 用于遞增觸發(fā)穴翩。
如在秒上面設(shè)置”5/15” 表示從5秒開(kāi)始,每增15秒觸(5,20,35,50)五督。
在月字段上設(shè)置’1/3’所示每月1號(hào)開(kāi)始,每隔三天觸發(fā)一次瓶殃。
- “L” 表示最后的意思充包。
在日字段設(shè)置上,表示當(dāng)月的最后一天(依據(jù)當(dāng)前月份遥椿,如果是二
月還會(huì)依據(jù)是否是潤(rùn)年[leap]), 在周字段上表示星期六基矮,相當(dāng)
于”7”或”SAT”。如果在”L”前加上數(shù)字冠场,則表示該數(shù)據(jù)的最后一個(gè)家浇。
例如在周字段上設(shè)置”6L”這樣的格式,則表示“本月最后一個(gè)星期五”
- “W” 表示離指定日期的最近那個(gè)工作日(周一至周五).
例如在日字段上置”15W”,表示離每月15號(hào)最近的那個(gè)工作日觸
發(fā)碴裙。如果15號(hào)正好是周六钢悲,則找最近的周五(14號(hào))觸發(fā), 如果15號(hào)
是周未,則找最近的下周一(16號(hào))觸發(fā).如果15號(hào)正好在工作日(周
一至周五)舔株,則就在該天觸發(fā)莺琳。如果指定格式為 “1W”,它則表示每
月1號(hào)往后最近的工作日觸發(fā)。如果1號(hào)正是周六载慈,則將在3號(hào)下周
一觸發(fā)惭等。(注,”W”前只能設(shè)置具體的數(shù)字,不允許區(qū)間”-“)办铡。
- “#”序號(hào)(表示每月的第幾個(gè)周幾).
例如在周字段上設(shè)置”6#3”表示在每月的第三個(gè)周六.注意如果指
定”#5”,正好第五周沒(méi)有周六辞做,則不會(huì)觸發(fā)該配置(用在母親節(jié)和父
親節(jié)再合適不過(guò)了) 琳要;小提示:’L’和 ‘W’可以一組合使用。如果在
日字段上設(shè)置”LW”,則表示在本月的最后一個(gè)工作日觸發(fā)秤茅;周字段
的設(shè)置稚补,若使用英文字母是不區(qū)分大小寫(xiě)的,即MON與mon相
同嫂伞。
通配符實(shí)例
為了幫助大家理解,我們?cè)谂e幾個(gè)例子:
- 每隔5秒執(zhí)行一次:*/5 * * * * ?
- 每隔1分鐘執(zhí)行一次:0 */1 * * * ?
- 每天23點(diǎn)執(zhí)行一次:0 0 23 * * ?
- 每天凌晨1點(diǎn)執(zhí)行一次:0 0 1 * * ?
- 每月1號(hào)凌晨1點(diǎn)執(zhí)行一次:0 0 1 1 * ?
- 每月最后一天23點(diǎn)執(zhí)行一次:0 0 23 L * ?
- 每周星期天凌晨1點(diǎn)實(shí)行一次:0 0 1 ? * L
- 在26分孔厉、29分、33分執(zhí)行一次:0 26,29,33 * * * ?
- 每天的0點(diǎn)帖努、13點(diǎn)撰豺、18點(diǎn)、21點(diǎn)都執(zhí)行一次:0 0 0,13,18,21 * * ?
cron定時(shí)任務(wù)實(shí)例
package com.mrcoder.sbschedule.job;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
@Configuration //1.主要用于標(biāo)記配置類拼余,兼?zhèn)銫omponent的效果污桦。
@EnableScheduling //2.開(kāi)啟定時(shí)任務(wù)
public class StaticSchedule {
//3.添加定時(shí)任務(wù)
//基于注解@Scheduled默認(rèn)為單線程
//開(kāi)啟多個(gè)任務(wù)時(shí),任務(wù)的執(zhí)行時(shí)機(jī)會(huì)受上一個(gè)任務(wù)執(zhí)行時(shí)間的影響匙监。
//這邊定時(shí)任務(wù)1延時(shí)10s來(lái)演示這個(gè)單線程的問(wèn)題
@Scheduled(cron = "0/5 * * * * ?")
private void configureTasks() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)1: " + LocalDateTime.now().toLocalTime());
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
System.err.println(ex);
}
}
@Scheduled(cron = "0/5 * * * * ?")
private void configureTasks2() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)2: " + LocalDateTime.now().toLocalTime());
}
}
八大參數(shù)-ZONE
時(shí)區(qū)凡橱,接收一個(gè)java.util.TimeZone#ID。cron表達(dá)式會(huì)基于該時(shí)區(qū)解析亭姥。默認(rèn)是一個(gè)空字符串稼钩,即取服務(wù)器所在地的時(shí)區(qū)。比如我們一般使用的時(shí)區(qū)Asia/Shanghai达罗。該字段我們一般留空坝撑。
使用實(shí)例
@Scheduled(cron = "0/5 * * * * ?", zone = "")
private void configureTasks() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)1: " + LocalDateTime.now().toLocalTime());
}
八大參數(shù)-fixedDelay
fixedDelay用于上一次執(zhí)行完畢時(shí)間點(diǎn)之后多長(zhǎng)時(shí)間再執(zhí)行
使用實(shí)例
//上一次執(zhí)行完畢時(shí)間點(diǎn)之后5秒再執(zhí)行
@Scheduled(fixedDelay = 5000)
private void configureTasks() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)1: " + LocalDateTime.now().toLocalTime());
}
八大參數(shù)-fixedDelayString(支持占位符)
與fixedDelay相同,只是使用字符串的形式粮揉。唯一不同的是支持占位符巡李。
使用實(shí)例
//上一次執(zhí)行完畢時(shí)間點(diǎn)之后5秒再執(zhí)行
@Scheduled(fixedDelayString = "5000")
private void configureTasks() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)1: " + LocalDateTime.now().toLocalTime());
}
八大參數(shù)-fixedRate
上一次開(kāi)始執(zhí)行時(shí)間點(diǎn)之后多長(zhǎng)時(shí)間再執(zhí)行
使用實(shí)例
//上一次開(kāi)始執(zhí)行時(shí)間點(diǎn)之后5秒再執(zhí)行
@Scheduled(fixedRate = 5000)
private void configureTasks() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)1: " + LocalDateTime.now().toLocalTime());
}
八大參數(shù)-fixedRateString(支持占位符)
與fixedRate相同,只是使用字符串的形式。唯一不同的是支持占位符扶认。
使用實(shí)例
//上一次開(kāi)始執(zhí)行時(shí)間點(diǎn)之后5秒再執(zhí)行
@Scheduled(fixedRateString = "5000")
private void configureTasks() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)1: " + LocalDateTime.now().toLocalTime());
}
八大參數(shù)-initialDelay
第一次延遲多長(zhǎng)時(shí)間后再執(zhí)行
使用實(shí)例
//第一次延遲1秒后執(zhí)行侨拦,之后按fixedRate的規(guī)則每5秒執(zhí)行一次
@Scheduled(initialDelay = 1000, fixedRate = 5000)
private void configureTasks() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)1: " + LocalDateTime.now().toLocalTime());
}
八大參數(shù)-initialDelayString(支持占位符)
與initialDelay相同,只是使用字符串的形式。唯一不同的是支持占位符辐宾。
使用實(shí)例
//第一次延遲1秒后執(zhí)行狱从,之后按fixedRate的規(guī)則每5秒執(zhí)行一次
@Scheduled(initialDelayString = "1000", fixedRate = 5000)
private void configureTasks() {
System.err.println("執(zhí)行靜態(tài)定時(shí)任務(wù)(單線程)1: " + LocalDateTime.now().toLocalTime());
}
基于注解(多線程)
這邊主要使用注解@EnableAsync開(kāi)啟多線程支持
使用注解@Async來(lái)表明定義一個(gè)異步的線程任務(wù)
案例
package com.mrcoder.sbschedule.job;
import org.springframework.scheduling.annotation.*;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
@EnableScheduling //1.開(kāi)啟定時(shí)任務(wù)
@EnableAsync //2.開(kāi)啟多線程,開(kāi)啟后多個(gè)定時(shí)任務(wù)不會(huì)互相影響
public class ThreadSchedule {
@Async
@Scheduled(fixedDelay = 1000) //間隔1秒
public void first() {
System.err.println("執(zhí)行定時(shí)任務(wù)(多線程)1 : " + LocalDateTime.now().toLocalTime() + "線程 : " + Thread.currentThread().getName());
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
System.err.println(ex);
}
}
@Async
@Scheduled(fixedDelay = 2000)
public void second() {
System.err.println("執(zhí)行定時(shí)任務(wù)(多線程)2 : " + LocalDateTime.now().toLocalTime() + "線程 : " + Thread.currentThread().getName());
}
}
基于接口SchedulingConfigurer(單/多線程)
上一小結(jié)介紹了基于注解@Scheduled的定時(shí)任務(wù),可以看到,很方便.但是有個(gè)致命的缺點(diǎn),
就是當(dāng)需要臨時(shí)調(diào)整定時(shí)任務(wù)執(zhí)行周期時(shí),必須要實(shí)時(shí)的重啟應(yīng)用才能生效.
那么能不能不重啟應(yīng)用就能“動(dòng)態(tài)”調(diào)整定時(shí)任務(wù)的執(zhí)行周期呢?
下面我們介紹基于接口SchedulingConfigurer的定時(shí)任務(wù)實(shí)現(xiàn)方式.
需要說(shuō)明的是,SchedulingConfigurer默認(rèn)使用的也是單線程的方式叠纹,如果需要配置多線程矫夯,則需要指定 PoolSize,加入如下代碼即可:
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
//10個(gè)線程
taskScheduler.setPoolSize(10);
taskScheduler.initialize();
taskRegistrar.setTaskScheduler(taskScheduler);
}
案例
定時(shí)任務(wù)周期庫(kù)
創(chuàng)建一張表用來(lái)存儲(chǔ)定時(shí)任務(wù)的執(zhí)行周期,后續(xù)調(diào)整周期時(shí)直接修改對(duì)應(yīng)的記錄即可
DROP DATABASE
IF
EXISTS `demo`;
CREATE DATABASE `demo`;
USE `demo`;
DROP TABLE
IF
EXISTS `crontab`;
CREATE TABLE `crontab` ( `cron_id` VARCHAR ( 30 ) NOT NULL PRIMARY KEY, `cron` VARCHAR ( 30 ) NOT NULL );
INSERT INTO `crontab`
VALUES
( '1', '0/10 * * * * ?' );
配置yml
spring:
datasource:
url: jdbc:mysql://127.0.01:3306/demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
實(shí)現(xiàn)SchedulingConfigurer接口
package com.mrcoder.sbschedule.job;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.config.TriggerTask;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Date;
@Component
//1.主要用于標(biāo)記配置類吊洼,兼?zhèn)銫omponent的效果训貌。
@Configuration
//2.開(kāi)啟定時(shí)任務(wù)
@EnableScheduling
public class DynamicSchedule implements SchedulingConfigurer {
@Mapper
public interface CronMapper {
@Select("select cron from crontab limit 1")
public String getCron();
}
//注入mapper
@Autowired
@SuppressWarnings("all")
CronMapper cronMapper;
/**
* 執(zhí)行定時(shí)任務(wù).
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
//多線程支持,線程池創(chuàng)建10個(gè)線程
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10);
taskScheduler.initialize();
taskRegistrar.setTaskScheduler(taskScheduler);
//定時(shí)任務(wù)1: 每1s執(zhí)行一次(每執(zhí)行一次睡眠10s)
taskRegistrar.addFixedRateTask(
new Runnable() {
//添加任務(wù)內(nèi)容
@Override
public void run() {
System.err.println("執(zhí)行動(dòng)態(tài)定時(shí)任務(wù)1: " + LocalDateTime.now().toLocalTime());
//為了驗(yàn)證多線程不阻塞,這邊睡眠 10s
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
System.err.println(ex);
}
}
}
, 1000);
//定時(shí)任務(wù)2: 讀取mysql中的執(zhí)行cron公式
TriggerTask triggrtTask = new TriggerTask(
//1.添加任務(wù)內(nèi)容(Runnable),這邊使用"拉姆達(dá)表達(dá)式"
() -> System.err.println("執(zhí)行動(dòng)態(tài)定時(shí)任務(wù)2: " + LocalDateTime.now().toLocalTime()),
//2.設(shè)置執(zhí)行周期(Trigger)
triggerContext -> {
//2.1 從數(shù)據(jù)庫(kù)獲取執(zhí)行周期
String cron = cronMapper.getCron();
//2.2 合法性校驗(yàn).
if (StringUtils.isEmpty(cron)) {
// Omitted Code ..
}
//2.3 返回執(zhí)行周期(Date)
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
taskRegistrar.addTriggerTask(triggrtTask);
}
}
案例代碼
https://github.com/MrCoderStack/SpringBootDemo/tree/master/sb-schedule