SpringBoot系列(六):使用SpringBoot定時任務(wù)時不得不采的坑


作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。

摘要:本文我們將分享介紹如何基于Spring Boot搭建的項(xiàng)目使用Spring Task定時任務(wù),帶領(lǐng)各位小伙伴踩一踩在使用Spring定時任務(wù)時所出現(xiàn)的坑,并采用線程池~多線程任務(wù)調(diào)度的形式對出現(xiàn)的坑加以解決、優(yōu)化!

內(nèi)容:對于定時任務(wù),相信各位小伙伴都有所耳聞,甚至有些小伙伴對定時任務(wù)的使用已經(jīng)到了“爐火純青”的地步!而事實(shí)上,在實(shí)際的項(xiàng)目、特別是企業(yè)級Spring的項(xiàng)目開發(fā)中, Spring Task定時任務(wù)的使用是相當(dāng)頻繁的。

本文我們將基于前文Spring Boot搭建的標(biāo)準(zhǔn)企業(yè)級項(xiàng)目作為奠基,采用注解的形式將Spring Task定時任務(wù)應(yīng)用到項(xiàng)目中,那廢話不多講,咱們直接進(jìn)入擼碼環(huán)節(jié)!

(1)首先,我們在 com.debug.springboot.server 包目錄下建立scheduler包目錄,并在其下建立一個通用化的用于編寫定時任務(wù)的CommonScheduler 類,如下源代碼所示,我們建立了三個定時任務(wù),其中每個定時任務(wù)執(zhí)行的時間頻率分別為每5s、每6s、每7s執(zhí)行一次:

/**
* spring task-定時任務(wù)調(diào)度
* @Author:debug (SteadyJack)
* @Date: 2019/9/7 11:05
**/
@Component
public class CommonScheduler {
private static final Logger log= LoggerFactory.getLogger(CommonScheduler.class);

//定時任務(wù)1
@Scheduled(cron = "0/5 * * * * *")
public void schedulerOne(){
log.info("---執(zhí)行定時任務(wù)1---");
}

//定時任務(wù)2
@Scheduled(cron = "0/6 * * * * *")
public void schedulerTwo(){
log.info("---執(zhí)行定時任務(wù)2---");

try {
//模擬當(dāng)前定時任務(wù)每次執(zhí)行業(yè)務(wù)邏輯時需要花費(fèi)的時間 3s
Thread.sleep(3000);
}catch (Exception e){e.printStackTrace();}
}

//定時任務(wù)3
@Scheduled(cron = "0/7 * * * * *")
public void schedulerThree(){
log.info("---執(zhí)行定時任務(wù)3---");

try {
//模擬當(dāng)前定時任務(wù)每次執(zhí)行業(yè)務(wù)邏輯時需要花費(fèi)的時間 4s
Thread.sleep(4000);
}catch (Exception e){e.printStackTrace();}
}
}

(2)其中,為了更好的模擬在實(shí)際項(xiàng)目開發(fā)中 定時任務(wù) 執(zhí)行的業(yè)務(wù)邏輯,我們假定了每次執(zhí)行定時任務(wù)2時需要花費(fèi)3s的時間,定時任務(wù)3執(zhí)行業(yè)務(wù)邏輯時需要花費(fèi)4s的時間。

理論上,每個定時任務(wù)在執(zhí)行相應(yīng)的業(yè)務(wù)邏輯時,是不應(yīng)該相互影響的,即在理想的情況下,SchedulerOne應(yīng)當(dāng)每隔5s執(zhí)行一次業(yè)務(wù)邏輯,SchedulerTwo應(yīng)當(dāng)每隔6s執(zhí)行一次業(yè)務(wù)邏輯,SchedulerThree應(yīng)當(dāng)每隔7s執(zhí)行一次業(yè)務(wù)邏輯,以此類推。

下面,我們將整個項(xiàng)目運(yùn)行起來,即可觸發(fā)這幾個定時任務(wù)調(diào)度的執(zhí)行。當(dāng)然,在此之前,我們需要在MainApplication啟動類中加入一個注解:@EnableScheduling 即允許定時任務(wù)調(diào)度的執(zhí)行,如下圖所示:


  (3)完了之后,即可將整個項(xiàng)目運(yùn)行起來了,耐心觀察控制臺Console的輸出信息,即可看到每個定時任務(wù)的執(zhí)行頻率,如下圖所示:  


(4)從該控制臺Console中,我們可以看到幾點(diǎn)信息:

A.第一點(diǎn)是所有的定時任務(wù)確實(shí)都已經(jīng)執(zhí)行了;

B.第二點(diǎn)是每個定時任務(wù)雖然都已經(jīng)執(zhí)行了,但是卻不是按照設(shè)定的cron來執(zhí)行,特別是SchedulerOne定時任務(wù)1,從上圖中會發(fā)現(xiàn)第一次執(zhí)行時是在 12:07:27 ,下次執(zhí)行時卻是在 12:07:35 ,前后間距為8s,而不是5s,這一點(diǎn)在我們看來是不正常的;

C.除此之外,還有最后一點(diǎn),即所有定時任務(wù)調(diào)度的執(zhí)行竟然都是在同一個“線程”內(nèi)執(zhí)行,這是很不可思議的,因?yàn)檫@會導(dǎo)致那些“cron相隔間距很短” 的定時任務(wù)出現(xiàn)堵塞的現(xiàn)象,這一點(diǎn)其實(shí)就是上述 B第二點(diǎn)出現(xiàn)的現(xiàn)象 的原因所在!

因此我們所講的 定時任務(wù) 在使用的過程中出現(xiàn)的坑,其實(shí)就是上述所講的第二點(diǎn)B跟第三點(diǎn)C,而為了解決這樣的問題(坑),下面我們將采用“線程池~多線程”的形式配置定時任務(wù)調(diào)度的執(zhí)行策略。

(1)首先,我們在 com.debug.springboot.server 包目錄下建立 config 包,然后在該包目錄中建立 “定時任務(wù)調(diào)度-線程池的通用配置類” SchedulerConfig ,其源代碼如下所示:

/**
* 定時任務(wù)調(diào)度-線程池配置
* @Author:debug (SteadyJack)
* @Date: 2019/9/7 11:12
**/
@Configuration
public class SchedulerConfig {

//任務(wù)調(diào)度線程池配置
@Bean("taskExecutor")
public Executor taskExecutor(){
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
//核心線程數(shù)
executor.setCorePoolSize(4);
//最大核心線程數(shù)
executor.setMaxPoolSize(10);
//設(shè)置隊(duì)列中等待被調(diào)度的任務(wù)的數(shù)量
executor.setQueueCapacity(8);
executor.initialize();
return executor;
}
}

  (2)完了之后,需要在通用的定時任務(wù)調(diào)度類中加入 該 taskBean 的使用,即主要加入兩個注解 @EnableAsync (允許異步執(zhí)行)、@Async("taskExecutor") (基于指定的配置Bean異步執(zhí)行相應(yīng)的業(yè)務(wù)邏輯),調(diào)整后的源代碼如下所示:  

/**
* spring task-定時任務(wù)調(diào)度
* @Author:debug (SteadyJack)
* @Date: 2019/9/7 11:05
**/
@Component
@EnableAsync
public class CommonScheduler {

private static final Logger log= LoggerFactory.getLogger(CommonScheduler.class);

//定時任務(wù)1
@Scheduled(cron = "0/5 * * * * *")
@Async("taskExecutor")
public void schedulerOne(){
log.info("---執(zhí)行定時任務(wù)1---");
}

//定時任務(wù)2
@Scheduled(cron = "0/6 * * * * *")
@Async("taskExecutor")
public void schedulerTwo(){
log.info("---執(zhí)行定時任務(wù)2---");

try {
//模擬當(dāng)前定時任務(wù)每次執(zhí)行業(yè)務(wù)邏輯時需要花費(fèi)的時間 3s
Thread.sleep(3000);
}catch (Exception e){e.printStackTrace();}
}

//定時任務(wù)3
@Scheduled(cron = "0/7 * * * * *")
@Async("taskExecutor")
public void schedulerThree(){
log.info("---執(zhí)行定時任務(wù)3---");

try {
//模擬當(dāng)前定時任務(wù)每次執(zhí)行業(yè)務(wù)邏輯時需要花費(fèi)的時間 4s
Thread.sleep(4000);
}catch (Exception e){e.printStackTrace();}
}
}


(3)將整個項(xiàng)目運(yùn)行起來,觀察控制臺Console的輸出,會發(fā)現(xiàn)此時控制臺的輸出信息跟沒使用“線程池~多線程”時是兩種結(jié)果,如下圖所示:


  至此,關(guān)于Spring Task定時任務(wù)調(diào)度在Spring Boot項(xiàng)目中的使用我們已經(jīng)介紹、實(shí)戰(zhàn)完畢了,實(shí)不相瞞,在實(shí)際的企業(yè)級應(yīng)用開發(fā)中,加入“線程池~多線程”的任務(wù)配置形式才是使用@Scheduled定時任務(wù)調(diào)度的正確方式!  


補(bǔ)充:

1、本文涉及到的相關(guān)的源代碼可以到此地址,check出來進(jìn)行查看學(xué)習(xí):

https://gitee.com/steadyjack/SpringBootTechnology

2、目前Debug已將本文所涉及的內(nèi)容整理錄制成視頻教程,感興趣的小伙伴可以前往觀看學(xué)習(xí):

https://www.fightjava.com/web/index/course/detail/5

3、關(guān)注一下Debug的技術(shù)微信公眾號唄,最新的技術(shù)文章、技術(shù)課程以及技術(shù)專欄將會第一時間在公眾號發(fā)布哦!