我們?cè)趯?shí)際開(kāi)發(fā)中,多多少少都會(huì)用到定時(shí)任務(wù)來(lái)處理一些問(wèn)題。
比如金融項(xiàng)目中的對(duì)賬,每天定時(shí)對(duì)昨天的賬務(wù)進(jìn)行核對(duì),每個(gè)月初對(duì)上個(gè)月的賬務(wù)進(jìn)行核對(duì)等。
還比如,我們需要處理一些老數(shù)據(jù)遷移,修復(fù)一些新項(xiàng)目和老項(xiàng)目數(shù)據(jù)不兼容的問(wèn)題等等。
常規(guī)實(shí)現(xiàn)方案
方案1:Timer
這個(gè)目前在項(xiàng)目中用得較少,直接貼demo代碼。
具體的介紹可以查看api ,但是在某些框架中是有用到。
public class TestTimer {
public static void main(String[] args){
TimerTask timerTask = new TimerTask() {
@Override
public void run(){
System.out.println("task run:"+ new Date());
}
};
Timer timer = new Timer();
//安排指定的任務(wù)在指定的時(shí)間開(kāi)始進(jìn)行重復(fù)的固定延遲執(zhí)行。這里是每3秒執(zhí)行一次
timer.schedule(timerTask,10,3000);
}
}
執(zhí)行結(jié)果:
task run:Sun Dec 11 21:23:47 CST 2022
task run:Sun Dec 11 21:23:50 CST 2022
task run:Sun Dec 11 21:23:53 CST 2022
這么使用,阿里代碼檢查插件會(huì)提示:

從提示中可以看出,在多線程并行處理定時(shí)任務(wù)時(shí),Timer運(yùn)行多個(gè)TimerTask時(shí),只要有其中之一沒(méi)有捕獲拋出的異常,其他任務(wù)會(huì)自動(dòng)終止運(yùn)行。
方案2:ScheduledExecutorService
和Timer類型,也就是阿里代碼檢查插件推薦的方案:
public class TestScheduledExecutorService {
public static void main(String[] args){
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
// 參數(shù):1、任務(wù)體 2、首次執(zhí)行的延時(shí)時(shí)間
// 3、任務(wù)執(zhí)行間隔 4、間隔時(shí)間單位
service.scheduleAtFixedRate(()->System.out.println("task ScheduledExecutorService "+new Date()), 0, 3, TimeUnit.SECONDS);
}
}
運(yùn)行結(jié)果:
task ScheduledExecutorService Sun Dec 11 21:30:06 CST 2022
task ScheduledExecutorService Sun Dec 11 21:30:09 CST 2022
task ScheduledExecutorService Sun Dec 11 21:30:12 CST 2022
阿里代碼檢查插件也會(huì)提示:

這里提示的是我們創(chuàng)建線程池的方式,建議我們使用手動(dòng)創(chuàng)建線程池,不要使用??Executors?
?工廠類,因?yàn)槭謩?dòng)創(chuàng)建更能有效規(guī)劃資源的使用。
方案3:spring task
用起來(lái)也非常簡(jiǎn)單:
@Slf4j
@Component
public class ScheduledService {
@Scheduled(cron = "0/5 * * * * *")
public void scheduled(){
log.info("=====>>>>>使用cron {}",System.currentTimeMillis());
}
@Scheduled(fixedRate = 5000)
public void scheduled1(){
log.info("=====>>>>>使用fixedRate{}", System.currentTimeMillis());
}
@Scheduled(fixedDelay = 5000)
public void scheduled2(){
log.info("=====>>>>>fixedDelay{}",System.currentTimeMillis());
}
}
運(yùn)行結(jié)果:
2022-12-11 21:36:25.001 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>使用cron 1670765785001
2022-12-11 21:36:28.212 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>使用fixedRate1670765788212
2022-12-11 21:36:28.212 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>fixedDelay1670765788212
2022-12-11 21:36:30.001 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>使用cron 1670765790001
2022-12-11 21:36:33.212 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>使用fixedRate1670765793212
2022-12-11 21:36:33.213 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>fixedDelay1670765793213
2022-12-11 21:36:35.001 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>使用cron 1670765795001
2022-12-11 21:36:38.214 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>使用fixedRate1670765798214
2022-12-11 21:36:38.214 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>fixedDelay1670765798214
2022-12-11 21:36:40.001 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>使用cron 1670765800001
2022-12-11 21:36:43.214 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>使用fixedRate1670765803214
2022-12-11 21:36:43.215 INFO 10660 --- [ scheduling-1] com.tian.utils.ScheduledService : =====>>>>>fixedDelay1670765803215
方案4:多線程執(zhí)行
基于注解設(shè)定多線程定時(shí)任務(wù) :
@Component
@EnableScheduling // 1.開(kāi)啟定時(shí)任務(wù)
@EnableAsync // 2.開(kāi)啟多線程
public class MultithreadScheduleTask {
@Async
@Scheduled(fixedDelay = 5000) //間隔5秒
public void first() throws InterruptedException {
System.out.println("第一個(gè)定時(shí)任務(wù)開(kāi)始 : " + LocalDateTime.now().toLocalTime() + "\r\n線程 : " + Thread.currentThread().getName());
System.out.println();
Thread.sleep(1000 * 10);
}
@Async
@Scheduled(fixedDelay = 5000)
public void second(){
System.out.println("第二個(gè)定時(shí)任務(wù)開(kāi)始 : " + LocalDateTime.now().toLocalTime() + "\r\n線程 : " + Thread.currentThread().getName());
System.out.println();
}
}
運(yùn)行結(jié)果:
第一個(gè)定時(shí)任務(wù)開(kāi)始 : 21:44:02.800
線程 : 入庫(kù)操作日志記錄表 線程1
第二個(gè)定時(shí)任務(wù)開(kāi)始 : 21:44:02.801
線程 : 入庫(kù)操作日志記錄表 線程2
第一個(gè)定時(shí)任務(wù)開(kāi)始 : 21:44:07.801
線程 : 入庫(kù)操作日志記錄表 線程3
第二個(gè)定時(shí)任務(wù)開(kāi)始 : 21:44:07.802
線程 : 入庫(kù)操作日志記錄表 線程4
第一個(gè)定時(shí)任務(wù)開(kāi)始 : 21:44:12.807
線程 : 入庫(kù)操作日志記錄表 線程5
第二個(gè)定時(shí)任務(wù)開(kāi)始 : 21:44:12.812
線程 : 入庫(kù)操作日志記錄表 線程6
......
方案5:quartz
我們需要引入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
實(shí)現(xiàn)類:
public class Myquartz extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("這是我的 quartz 定時(shí)任務(wù)");
}
}
配置類:
/**
* @author tianwc 公眾號(hào):java后端技術(shù)全棧、面試專欄
* @version 1.0.0
* @date 2022年12月11日 21:48
*/
@Configuration
public class QuartzConfig {
@Bean
public JobDetail teatQuartzDetail(){
return JobBuilder.newJob(MyQuartz.class).withIdentity("myQuartz").storeDurably().build();
}
@Bean
public Trigger testQuartzTrigger(){
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(10) //設(shè)置時(shí)間周期單位秒
.repeatForever();
return TriggerBuilder.newTrigger().forJob(teatQuartzDetail())
.withIdentity("testQuartz")
.withSchedule(scheduleBuilder)
.build();
}
}
只要啟動(dòng)Spring Boot項(xiàng)目,就會(huì)輸出:
這是我的 quartz 定時(shí)任務(wù)
這是我的 quartz 定時(shí)任務(wù)
這是我的 quartz 定時(shí)任務(wù)
其他方案
我們?cè)陧?xiàng)目,可能會(huì)涉及動(dòng)態(tài)調(diào)整定時(shí)任務(wù)執(zhí)行core表達(dá)式、動(dòng)態(tài)關(guān)閉開(kāi)啟定時(shí)任務(wù),我們可以使用??SchedulingConfigurer?
?來(lái)實(shí)現(xiàn)(使用數(shù)據(jù)庫(kù)結(jié)合來(lái)搞):
比如:
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
@Autowired
private ApplicationContext context;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar){
for (SpringScheduledCron springScheduledCron : cronRepository.findAll()) {
Class<?> clazz;
Object task;
try {
clazz = Class.forName(springScheduledCron.getCronKey());
task = context.getBean(clazz);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("spring_scheduled_cron表數(shù)據(jù)" + springScheduledCron.getCronKey() + "有誤", e);
} catch (BeansException e) {
throw new IllegalArgumentException(springScheduledCron.getCronKey() + "未納入到spring管理", e);
}
Assert.isAssignable(ScheduledOfTask.class, task.getClass(), "定時(shí)任務(wù)類必須實(shí)現(xiàn)ScheduledOfTask接口");
// 可以通過(guò)改變數(shù)據(jù)庫(kù)數(shù)據(jù)進(jìn)而實(shí)現(xiàn)動(dòng)態(tài)改變執(zhí)行周期
taskRegistrar.addTriggerTask(((Runnable) task),
triggerContext -> {
//這個(gè)可以使用持久層,比如Mybatis來(lái)實(shí)現(xiàn),從數(shù)據(jù)庫(kù)中獲取
String cronExpression = "0/10 * * * * ? "
return new CronTrigger(cronExpression).nextExecutionTime(triggerContext);
}
);
}
}
@Bean
public Executor taskExecutor(){
return Executors.newScheduledThreadPool(10);
}
}
如果項(xiàng)目中用得到類似的,可以網(wǎng)上搜搜SchedulingConfigurer便可實(shí)現(xiàn)。
進(jìn)而再擴(kuò)展,那就來(lái)到分布式任務(wù)調(diào)度了。
什么是分布式任務(wù)調(diào)度?
任務(wù)調(diào)度是指基于給定的時(shí)間點(diǎn),給定的時(shí)間間隔或者給定執(zhí)行次數(shù)自動(dòng)得執(zhí)行任務(wù)。任務(wù)調(diào)度是是操作系統(tǒng)的重要組成部分,而對(duì)于實(shí)時(shí)的操作系統(tǒng),任務(wù)調(diào)度直接影響著操作系統(tǒng)的實(shí)時(shí)性能。任務(wù)調(diào)度涉及到多線程并發(fā)、運(yùn)行時(shí)間規(guī)則定制及解析、線程池的維護(hù)等諸多方面的工作。
WEB服務(wù)器在接受請(qǐng)求時(shí),會(huì)創(chuàng)建一個(gè)新的線程服務(wù)。但是資源有限,必須對(duì)資源進(jìn)行控制,首先就是限制服務(wù)線程的最大數(shù)目,其次考慮以線程池共享服務(wù)的線程資源,降低頻繁創(chuàng)建、銷毀線程的消耗;然后任務(wù)調(diào)度信息的存儲(chǔ)包括運(yùn)行次數(shù)、調(diào)度規(guī)則以及運(yùn)行數(shù)據(jù)等。一個(gè)合適的任務(wù)調(diào)度框架對(duì)于項(xiàng)目的整體性能來(lái)說(shuō)顯得尤為重要。
分布式任務(wù)調(diào)度框架有:cronsun、Elastic-job、saturn、lts、TBSchedule、xxl-job 等。
另外,就是cron表達(dá)式,推薦 http://www.pppet.net/

可以根據(jù)自己業(yè)務(wù)情況來(lái),手動(dòng)選擇,自動(dòng)生成表達(dá)式。