重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

重學(xué)springBoot系列之異步任務(wù)與定時(shí)任務(wù)實(shí)現(xiàn)Async異步任務(wù)環(huán)境準(zhǔn)備同步調(diào)用異步調(diào)用異步回調(diào)為異步任務(wù)規(guī)劃線程spring boot任務(wù)線程池自定義線程池優(yōu)雅地關(guān)閉線程池通過@Scheduled實(shí)現(xiàn)定時(shí)任務(wù)開啟定時(shí)任務(wù)方法不同定時(shí)方式的解析1.fixedDelay和fixedRate,單位是毫秒,它們的區(qū)別就是:cron表達(dá)式:靈活實(shí)現(xiàn)定時(shí)任務(wù)解決定時(shí)任務(wù)單線程運(yùn)行的問題quartz簡單定時(shí)任務(wù)(內(nèi)存持久化)引入對應(yīng)的 maven依賴創(chuàng)建一個(gè)任務(wù)類Job創(chuàng)建 Quartz 定時(shí)配置類深入解析核心概念SimpleTrigger and CronTriggerquartz動態(tài)定時(shí)任務(wù)(數(shù)據(jù)庫持久化)前言原理配置動態(tài)配置代碼實(shí)現(xiàn)分布式任務(wù)調(diào)度框架—xxl-job實(shí)現(xiàn)Async異步任務(wù)環(huán)境準(zhǔn)備

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

在 spring boot 入口類上配置 @enableasync 注解開啟異步處理。 創(chuàng)建任務(wù)抽象類 abstracttask,并分別配置三個(gè)任務(wù)方法 dotaskone(),dotasktwo(),dotaskthree()。

代碼語言:Javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

public abstract class AbstractTask {    private static Random random = new Random();    public void doTaskOne() throws Exception {        System.out.println("開始做任務(wù)一");        long start = currentTimeMillis();        sleep(random.nextInt(10000));        long end = currentTimeMillis();        System.out.println("完成任務(wù)一,耗時(shí):" + (end - start) + "毫秒");    }    public void doTaskTwo() throws Exception {        System.out.println("開始做任務(wù)二");        long start = currentTimeMillis();        sleep(random.nextInt(10000));        long end = currentTimeMillis();        System.out.println("完成任務(wù)二,耗時(shí):" + (end - start) + "毫秒");    }    public void doTaskThree() throws Exception {        System.out.println("開始做任務(wù)三");        long start = currentTimeMillis();        sleep(random.nextInt(10000));        long end = currentTimeMillis();        System.out.println("完成任務(wù)三,耗時(shí):" + (end - start) + "毫秒");    }}

同步調(diào)用

下面通過一個(gè)簡單示例來直觀的理解什么是同步調(diào)用:

定義 Task 類,繼承 AbstractTask,三個(gè)處理函數(shù)分別模擬三個(gè)執(zhí)行任務(wù)的操作,操作消耗時(shí)間隨機(jī)取(10 秒內(nèi))。代碼語言:JavaScript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Componentpublic class SyncTask extends AbstractTask {}

在 單元測試 用例中,注入 SyncTask 對象,并在測試用例中執(zhí)行 doTaskOne(),doTaskTwo(),doTaskThree() 三個(gè)方法。代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@RunWith(SpringRunner.class)@SpringBootTestpublic class TaskTest {    @Autowired    private SyncTask task;    @Test    public void testSyncTasks() throws Exception {        task.doTaskOne();        task.doTaskTwo();        task.doTaskThree();    }}

執(zhí)行單元測試,可以看到類似如下輸出:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

開始做任務(wù)一完成任務(wù)一,耗時(shí):6720毫秒開始做任務(wù)二完成任務(wù)二,耗時(shí):6604毫秒開始做任務(wù)三完成任務(wù)三,耗時(shí):9448毫秒
重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

任務(wù)一、任務(wù)二、任務(wù)三順序的執(zhí)行完了,換言之 doTaskOne(),doTaskTwo(),doTaskThree() 三個(gè)方法按調(diào)用順序的先后執(zhí)行完成。


異步調(diào)用

上述的 同步調(diào)用 雖然順利的執(zhí)行完了三個(gè)任務(wù),但是可以看到 執(zhí)行時(shí)間比較長,若這三個(gè)任務(wù)本身之間 不存在依賴關(guān)系,可以 并發(fā)執(zhí)行 的話,同步調(diào)用在 執(zhí)行效率 方面就比較差,可以考慮通過 異步調(diào)用 的方式來 并發(fā)執(zhí)行。

在Application啟動類上面加上@EnableAsync 創(chuàng)建 AsyncTask類,分別在方法上配置 @Async 注解,將原來的 同步方法 變?yōu)?異步方法。代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Componentpublic class SyncTask extends AbstractTask {    @Async    @Override    public void doTaskOne() throws Exception {        super.doTaskOne();    }    @Async    @Override    public void doTaskTwo() throws Exception {        super.doTaskTwo();    }    @Async    @Override    public void doTaskThree() throws Exception {        super.doTaskThree();    }}

在 單元測試 用例中,注入 AsyncTask 對象,并在測試用例中執(zhí)行 doTaskOne(),doTaskTwo(),doTaskThree() 三個(gè)方法。代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Autowiredprivate AsyncTask asyncTask;@Testpublic void testAsyncTasks() throws Exception {    asyncTask.doTaskOne();    asyncTask.doTaskTwo();    asyncTask.doTaskThree();}

執(zhí)行單元測試,可以看到類似如下輸出:代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

開始做任務(wù)三開始做任務(wù)一開始做任務(wù)二

如果反復(fù)執(zhí)行單元測試,可能會遇到各種不同的結(jié)果,比如:

沒有任何任務(wù)相關(guān)的輸出 有部分任務(wù)相關(guān)的輸出 亂序的任務(wù)相關(guān)的輸出

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

原因是目前 doTaskOne(),doTaskTwo(),doTaskThree() 這三個(gè)方法已經(jīng) 異步并發(fā)執(zhí)行 了。主程序在 異步調(diào)用 之后,主程序并不會理會這三個(gè)函數(shù)是否執(zhí)行完成了,由于沒有其他需要執(zhí)行的內(nèi)容,所以程序就 自動結(jié)束 了,導(dǎo)致了任務(wù) 不完整 或是 沒有輸出 相關(guān)內(nèi)容的情況。


異步回調(diào)

為了讓 doTaskOne(),doTaskTwo(),doTaskThree() 能正常結(jié)束,假設(shè)我們需要統(tǒng)計(jì)一下三個(gè)任務(wù) 并發(fā)執(zhí)行 共耗時(shí)多少,這就需要等到上述三個(gè)函數(shù)都完成動用之后記錄時(shí)間,并計(jì)算結(jié)果。

那么我們?nèi)绾闻袛嗌鲜鋈齻€(gè) 異步調(diào)用 是否已經(jīng)執(zhí)行完成呢?我們需要使用 Future 來返回 異步調(diào)用 的 結(jié)果。

創(chuàng)建 AsyncCallBackTask 類,聲明 doTaskOneCallback(),doTaskTwoCallback(),doTaskThreeCallback() 三個(gè)方法,對原有的三個(gè)方法進(jìn)行包裝。代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Componentpublic class AsyncCallBackTask extends AbstractTask {    @Async    public Future<String> doTaskOneCallback() throws Exception {        super.doTaskOne();        return new AsyncResult<>("任務(wù)一完成");    }    @Async    public Future<String> doTaskTwoCallback() throws Exception {        super.doTaskTwo();        return new AsyncResult<>("任務(wù)二完成");    }    @Async    public Future<String> doTaskThreeCallback() throws Exception {        super.doTaskThree();        return new AsyncResult<>("任務(wù)三完成");    }}

在 單元測試 用例中,注入 AsyncCallBackTask 對象,并在測試用例中執(zhí)行 doTaskOneCallback(),doTaskTwoCallback(),doTaskThreeCallback() 三個(gè)方法。循環(huán)調(diào)用 Future 的 isDone() 方法等待三個(gè) 并發(fā)任務(wù) 執(zhí)行完成,記錄最終執(zhí)行時(shí)間。代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Autowiredprivate AsyncCallBackTask asyncCallBackTask;@Testpublic void testAsyncCallbackTask() throws Exception {    long start = currentTimeMillis();    Future<String> task1 = asyncCallBackTask.doTaskOneCallback();    Future<String> task2 = asyncCallBackTask.doTaskTwoCallback();    Future<String> task3 = asyncCallBackTask.doTaskThreeCallback();    // 三個(gè)任務(wù)都調(diào)用完成,退出循環(huán)等待    while (!task1.isDone() || !task2.isDone() || !task3.isDone()) {        sleep(1000);    }    long end = currentTimeMillis();    System.out.println("任務(wù)全部完成,總耗時(shí):" + (end - start) + "毫秒");}

看看都做了哪些改變:

在測試用例一開始記錄開始時(shí)間; 在調(diào)用三個(gè)異步函數(shù)的時(shí)候,返回Future類型的結(jié)果對象; 在調(diào)用完三個(gè)異步函數(shù)之后,開啟一個(gè)循環(huán),根據(jù)返回的Future對象來判斷三個(gè)異步函數(shù)是否都結(jié)束了。若都結(jié)束,就結(jié)束循環(huán);若沒有都結(jié)束,就等1秒后再判斷。 跳出循環(huán)之后,根據(jù)結(jié)束時(shí)間 – 開始時(shí)間,計(jì)算出三個(gè)任務(wù)并發(fā)執(zhí)行的總耗時(shí)。

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

執(zhí)行一下上述的單元測試,可以看到如下結(jié)果:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

開始做任務(wù)三開始做任務(wù)一開始做任務(wù)二完成任務(wù)二,耗時(shí):2572毫秒完成任務(wù)一,耗時(shí):7333毫秒完成任務(wù)三,耗時(shí):7647毫秒任務(wù)全部完成,總耗時(shí):8013毫秒

可以看到,通過 異步調(diào)用,讓任務(wù)一、任務(wù)二、任務(wù)三 并發(fā)執(zhí)行,有效的 減少 了程序的 運(yùn)行總時(shí)間。


為異步任務(wù)規(guī)劃線程池Spring Boot任務(wù)線程池

線程池的作用

防止資源占用無限的擴(kuò)張 調(diào)用過程省去資源的創(chuàng)建和銷毀所占用的時(shí)間

在上一節(jié)中,我們的一個(gè)異步任務(wù)打開了一個(gè)線程,完成后銷毀。在高并發(fā)環(huán)境下,不斷的分配新資源,可能導(dǎo)致系統(tǒng)資源耗盡。所以為了避免這個(gè)問題,我們?yōu)楫惒饺蝿?wù)規(guī)劃一個(gè)線程池。當(dāng)然,如果沒有配置線程池的話,springboot會自動配置一個(gè)ThreadPoolTaskExecutor 線程池到bean當(dāng)中。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

# 核心線程數(shù)spring.task.execution.pool.core-size=8  # 最大線程數(shù)spring.task.execution.pool.max-size=16# 空閑線程存活時(shí)間spring.task.execution.pool.keep-alive=60s# 是否允許核心線程超時(shí)spring.task.execution.pool.allow-core-thread-timeout=true# 線程隊(duì)列數(shù)量spring.task.execution.pool.queue-capacity=100# 線程關(guān)閉等待spring.task.execution.shutdown.await-termination=falsespring.task.execution.shutdown.await-termination-period=# 線程名稱前綴spring.task.execution.thread-name-prefix=task-

自定義線程池

有的時(shí)候,我們希望將系統(tǒng)內(nèi)的一類任務(wù)放到一個(gè)線程池,另一類任務(wù)放到另外一個(gè)線程池,所以使用Spring Boot自帶的任務(wù)線程池就捉襟見肘了。下面介紹自定義線程池的方法。

創(chuàng)建一個(gè) 線程池配置類TaskConfiguration ,并配置一個(gè) 任務(wù)線程池對象taskExecutor。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Configurationpublic class TaskConfiguration {    @Bean("taskExecutor")    public Executor taskExecutor() {        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();        executor.setCorePoolSize(10);        executor.setMaxPoolSize(20);        executor.setQueueCapacity(200);        executor.setKeepAliveSeconds(60);        executor.setThreadNamePrefix("taskExecutor-");        executor.setRejectedExecutionHandler(new CallerRunsPolicy());        return executor;    }}
重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

上面我們通過使用 ThreadPoolTaskExecutor 創(chuàng)建了一個(gè) 線程池,同時(shí)設(shè)置了以下這些參數(shù):

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

Reject策略預(yù)定義有四種:

AbortPolicy,用于被拒絕任務(wù)的處理程序,它將拋出RejectedExecutionException。 CallerRunsPolicy,用于被拒絕任務(wù)的處理程序,它直接在execute方法的調(diào)用線程中運(yùn)行被拒絕的任務(wù)。 DiscardOldestPolicy,用于被拒絕任務(wù)的處理程序,它放棄最舊的未處理請求,然后重試execute。 DiscardPolicy,用于被拒絕任務(wù)的處理程序,默認(rèn)情況下它將丟棄被拒絕的任務(wù)。

創(chuàng)建 AsyncExecutorTask類,三個(gè)任務(wù)的配置和 AsyncTask 一樣,不同的是 @Async 注解需要指定前面配置的 線程池的名稱taskExecutor。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Componentpublic class AsyncExecutorTask extends AbstractTask {    @Async("taskExecutor")    public Future<String> doTaskOneCallback() throws Exception {        super.doTaskOne();        System.out.println("任務(wù)一,當(dāng)前線程:" + Thread.currentThread().getName());        return new AsyncResult<>("任務(wù)一完成");    }    @Async("taskExecutor")    public Future<String> doTaskTwoCallback() throws Exception {        super.doTaskTwo();        System.out.println("任務(wù)二,當(dāng)前線程:" + Thread.currentThread().getName());        return new AsyncResult<>("任務(wù)二完成");    }    @Async("taskExecutor")    public Future<String> doTaskThreeCallback() throws Exception {        super.doTaskThree();        System.out.println("任務(wù)三,當(dāng)前線程:" + Thread.currentThread().getName());        return new AsyncResult<>("任務(wù)三完成");    }}

在 單元測試 用例中,注入 AsyncExecutorTask 對象,并在測試用例中執(zhí)行 doTaskOne(),doTaskTwo(),doTaskThree() 三個(gè)方法。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@SpringBootTestpublic class AsyncExecutorTaskTest {    @Autowired    private AsyncExecutorTask task;    @Test    public void testAsyncExecutorTask() throws Exception {        task.doTaskOneCallback();        task.doTaskTwoCallback();        task.doTaskThreeCallback();        sleep(30 * 1000L);    }}

執(zhí)行一下上述的 單元測試,可以看到如下結(jié)果:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

開始做任務(wù)一開始做任務(wù)三開始做任務(wù)二完成任務(wù)二,耗時(shí):3905毫秒任務(wù)二,當(dāng)前線程:taskExecutor-2完成任務(wù)一,耗時(shí):6184毫秒任務(wù)一,當(dāng)前線程:taskExecutor-1完成任務(wù)三,耗時(shí):9737毫秒任務(wù)三,當(dāng)前線程:taskExecutor-3

執(zhí)行上面的單元測試,觀察到 任務(wù)線程池 的 線程池名的前綴 被打印,說明 線程池 成功執(zhí)行 異步任務(wù)!


優(yōu)雅地關(guān)閉線程池

解決方案如下,重新設(shè)置線程池配置對象,新增線程池 setWaitForTasksToCompleteOnShutdown() 和 setAwaitTerminationSeconds() 配置:

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Bean("taskExecutor")public Executor taskExecutor() {    ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();    executor.setPoolSize(20);    executor.setThreadNamePrefix("taskExecutor-");    executor.setWaitForTasksToCompleteOnShutdown(true);    executor.setAwaitTerminationSeconds(60);    return executor;}

setWaitForTasksToCompleteOnShutdown(true): 該方法用來設(shè)置 線程池關(guān)閉 的時(shí)候 等待 所有任務(wù)都完成后,再繼續(xù) 銷毀 其他的 Bean,這樣這些 異步任務(wù) 的 銷毀 就會先于 數(shù)據(jù)庫連接池對象 的銷毀。 setAwaitTerminationSeconds(60): 該方法用來設(shè)置線程池中 任務(wù)的等待時(shí)間,如果超過這個(gè)時(shí)間還沒有銷毀就 強(qiáng)制銷毀,以確保應(yīng)用最后能夠被關(guān)閉,而不是阻塞住。


通過@Scheduled實(shí)現(xiàn)定時(shí)任務(wù)開啟定時(shí)任務(wù)方法

Scheduled定時(shí)任務(wù)是Spring boot自身提供的功能,所以不需要引入Maven依賴包

在項(xiàng)目入口main方法上加注解

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@EnableScheduling //開啟定時(shí)任務(wù)

不同定時(shí)方式的解析1.fixedDelay和fixedRate,單位是毫秒,它們的區(qū)別就是:

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

fixedRate就是每隔多長時(shí)間執(zhí)行一次。(開始——->X時(shí)間——>再開始)。如果間隔時(shí)間小于任務(wù)執(zhí)行時(shí)間,上一次任務(wù)執(zhí)行完成下一次任務(wù)就立即執(zhí)行。如果間隔時(shí)間大于任務(wù)執(zhí)行時(shí)間,就按照每隔X時(shí)間運(yùn)行一次。 而fixedDelay是當(dāng)任務(wù)執(zhí)行完畢后一段時(shí)間再次執(zhí)行。(開始—>結(jié)束(隔一分鐘)開始—–>結(jié)束)。上一次執(zhí)行任務(wù)未完成,下一次任務(wù)不會開始


cron表達(dá)式:靈活

舉例說明

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

第一位,表示秒,取值0-59 第二位,表示分,取值0-59 第三位,表示小時(shí),取值0-23 第四位,日期天/日,取值1-31 第五位,日期月份,取值1-12 第六位,星期,取值1-7,星期一,星期二…,注:不是第1周,第二周的意思,另外:1表示星期天,2表示星期一。 第七位,年份,可以留空,取值1970-2099

cron中,還有一些特殊的符號,含義如下:

(*)星號:可以理解為每的意思,每秒,每分,每天,每月,每年… (?)問號:問號只能出現(xiàn)在日期和星期這兩個(gè)位置。 (-)減號:表達(dá)一個(gè)范圍,如在小時(shí)字段中使用“10-12”,則表示從10到12點(diǎn),即10,11,12 (,)逗號:表達(dá)一個(gè)列表值,如在星期字段中使用“1,2,4”,則表示星期一,星期二,星期四 (/)斜杠:如:x/y,x是開始值,y是步長,比如在第一位(秒)0/15就是,從0秒開始,每15秒,最后就是0,15,30,45,60 另:/y,等同于0/y

cron表達(dá)式在線:http://cron.qqe2.com/


實(shí)現(xiàn)定時(shí)任務(wù)代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Componentpublic class ScheduledJobs {      //表示方法執(zhí)行完成后5秒再開始執(zhí)行    @Scheduled(fixedDelay=5000)    public void fixedDelayJob() throws InterruptedException{        System.out.println("fixedDelay 開始:" + new Date());        Thread.sleep(10 * 1000);        System.out.println("fixedDelay 結(jié)束:" + new Date());    }        //表示每隔3秒    @Scheduled(fixedRate=3000)    public void fixedRateJob()throws InterruptedException{        System.out.println("===========fixedRate 開始:" + new Date());        Thread.sleep(5 * 1000);        System.out.println("===========fixedRate 結(jié)束:" + new Date());    }    //表示每隔10秒執(zhí)行一次    @Scheduled(cron="0/10 * * * * ? ")    public void cronJob(){        System.out.println("=========================== ...>>cron...." + new Date());    }}

運(yùn)行結(jié)果如下:從運(yùn)行結(jié)果上看,并未按照預(yù)期的時(shí)間規(guī)律運(yùn)行。仔細(xì)看線程打印,竟然所有的定時(shí)任務(wù)使用的都是一個(gè)線程,所以彼此互相影響。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

===========fixedRate 結(jié)束:Tue Jul 09 19:53:04 CST 2019pool-1-thread-1fixedDelay 開始:Tue Jul 09 19:53:04 CST 2019pool-1-thread-1fixedDelay 結(jié)束:Tue Jul 09 19:53:14 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:14 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:16 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:16 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:18 CST 2019pool-1-thread-1=========================== ...>>cron....Tue Jul 09 19:53:18 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:18 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:20 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:20 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:22 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:22 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:24 CST 2019pool-1-thread-1fixedDelay 開始:Tue Jul 09 19:53:24 CST 2019pool-1-thread-1fixedDelay 結(jié)束:Tue Jul 09 19:53:34 CST 2019pool-1-thread-1=========================== ...>>cron....Tue Jul 09 19:53:34 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:34 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:36 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:36 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:38 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:38 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:40 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:40 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:42 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:42 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:44 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:44 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:46 CST 2019pool-1-thread-1===========fixedRate 開始:Tue Jul 09 19:53:46 CST 2019pool-1-thread-1===========fixedRate 結(jié)束:Tue Jul 09 19:53:48 CST 2019pool-1-thread-1fixedDelay 開始:Tue Jul 09 19:53:48 CST 2019pool-1-thread-1fixedDelay 結(jié)束:Tue Jul 09 19:53:58 CST 2019pool-1-thread-1=========================== ...>>cron....Tue Jul 09 19:53:58 CST 2019pool-1-thread-1

解決定時(shí)任務(wù)單線程運(yùn)行的問題代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Configuration@EnableSchedulingpublic class ScheduleConfig implements SchedulingConfigurer {     @Override    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {        taskRegistrar.setScheduler(scheduledTaskExecutor());    }     @Bean    public Executor scheduledTaskExecutor() {        return Executors.newScheduledThreadPool(3); //指定線程池大小    }}

再次運(yùn)行上面的程序,運(yùn)行時(shí)間規(guī)律就符合期望了。


quartz簡單定時(shí)任務(wù)(內(nèi)存持久化)

Quartz是OpenSymphony開源組織在工作計(jì)劃-定時(shí)任務(wù)領(lǐng)域的另一個(gè)開源項(xiàng)目。它是完全由Java開發(fā)的,可用于執(zhí)行預(yù)定任務(wù)。它類似于java.util.Timer定時(shí)器。但是與timer相比,quartz增加了許多功能。

引入對應(yīng)的 maven依賴

在 springboot2.0 后官方添加了 Quartz 框架的依賴,所以只需要在 pom 文件當(dāng)中引入

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

      <!--引入quartz定時(shí)框架-->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-quartz</artifactId>        </dependency>

創(chuàng)建一個(gè)任務(wù)類Job

首先,我們需要定義一個(gè)接口來實(shí)現(xiàn)計(jì)時(shí)功能。我們可以將其稱為任務(wù)(或任務(wù)),例如:定期發(fā)送電子郵件的任務(wù),重新啟動機(jī)器的任務(wù)以及在優(yōu)惠券到期時(shí)發(fā)送SMS提醒的任務(wù)。

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

由于 springboot2.0 自動進(jìn)行了依賴所以創(chuàng)建的定時(shí)任務(wù)類直接繼承 QuzrtzJobBean 就可以了,新建一個(gè)定時(shí)任務(wù)類:QuartzSimpleTask

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

public class QuartzSimpleTask extends QuartzJobBean {    @Override    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {        System.out.println("quartz簡單的定時(shí)任務(wù)執(zhí)行時(shí)間:"+new Date().toLocaleString());    }}

創(chuàng)建 Quartz 定時(shí)配置類

還需要一個(gè)可以觸發(fā)任務(wù)執(zhí)行的觸發(fā)器。觸發(fā)器觸發(fā)器的基本功能是指定作業(yè)的執(zhí)行時(shí)間,執(zhí)行間隔和運(yùn)行時(shí)間。

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

如何結(jié)合工作與觸發(fā)?也就是說,如何分配觸發(fā)器以執(zhí)行指定的作業(yè)?此時(shí),需要一個(gè)Schedule來實(shí)現(xiàn)此功能。

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

將之前創(chuàng)建的定時(shí)任務(wù)添加到定時(shí)調(diào)度里面

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Configurationpublic class QuartzSimpleConfig {    //指定具體的定時(shí)任務(wù)類    @Bean    public JobDetail uploadTaskDetail() {        return JobBuilder.newJob(QuartzSimpleTask.class)                        .withIdentity("QuartzSimpleTask")                        .storeDurably().build();    }    @Bean    public Trigger uploadTaskTrigger() {        //這里設(shè)定觸發(fā)執(zhí)行的方式        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("*/5 * * * * ?");        // 返回任務(wù)觸發(fā)器        return TriggerBuilder.newTrigger().forJob(uploadTaskDetail())                .withIdentity("QuartzSimpleTask")                .withSchedule(scheduleBuilder)                .build();    }}

最后運(yùn)行項(xiàng)目查看效果,”*/5 * * * * ?”表示定時(shí)任務(wù),每隔5秒鐘執(zhí)行一次。

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

只要項(xiàng)目一啟動,定時(shí)任務(wù)就會開始執(zhí)行


深入解析核心概念 Job:一個(gè)僅包含一個(gè)void execute(JobExecutionContext context)Abstract方法的簡單接口。在實(shí)際開發(fā)中,要執(zhí)行的任務(wù)是通過實(shí)現(xiàn)接口自定義實(shí)現(xiàn)的。JobExecutionContext提供調(diào)度上下文信息。代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

public interface Job {    void execute(JobExecutionContext context)     throws JobExecutionException;}

JobDetail:包含多個(gè)構(gòu)造函數(shù),最常用的是JobDetail(String name, String group,Class jobClass),Jobclass是實(shí)現(xiàn)作業(yè)接口的類,name是調(diào)度程序中任務(wù)的名稱,group是調(diào)度程序中任務(wù)的組名。默認(rèn)組名稱為Scheduler.DEFAULT_GROUP。 Trigger:描述觸發(fā)作業(yè)執(zhí)行的時(shí)間規(guī)則的類。包含:

SimpleTrigger:一次或固定間隔時(shí)間段的觸發(fā)規(guī)則。

CronTrigger:通過cron表達(dá)式描述更復(fù)雜的觸發(fā)規(guī)則。

Calendar:Quartz 提供的Calendar類。觸發(fā)器可以與多個(gè)Calendar關(guān)聯(lián)以排除特殊日期。

Scheduler:代表獨(dú)立于Quartz 的運(yùn)行容器。在Scheduler 中注冊了Trigger和JobDetail。它們在調(diào)度程序中具有自己的名稱(名稱)和組名稱(Group)。觸發(fā)器和JobDetail名稱和組名稱的組合必須唯一,但是觸發(fā)器名稱和組名稱的組合可以與JobDetail相同。一個(gè)Job可以綁定到多個(gè)觸發(fā)器,也可以不綁定。


Job還具有一個(gè)子接口:statefuljob,這是一個(gè)沒有方法的標(biāo)簽接口,表示有狀態(tài)任務(wù)。

無狀態(tài)任務(wù):它具有jobdatamap復(fù)制,因此可以并發(fā)運(yùn)行; 有狀態(tài)任務(wù)statefuljob:共享一個(gè)jobdatamap,并且將保存對jobdatamap的每次修改。因此,前一個(gè)有statefuljob將阻止下一個(gè)statefuljob。


SimpleTrigger and CronTrigger SimpleTrigger可以在指定的時(shí)間段內(nèi)執(zhí)行一個(gè)Job任務(wù),也可以在一個(gè)時(shí)間段內(nèi)多次執(zhí)行。 CronTrigger功能非常強(qiáng)大,它基于Calendar進(jìn)行作業(yè)調(diào)度,并且可以比simpletrigger更精確地指定間隔,因此crotrigger比simpletrigger更常用。Crotrigger基于cron表達(dá)式。

首先,讓我們了解cron表達(dá)式: 由七個(gè)子表達(dá)式組成的字符串的格式如下:

[秒] [分鐘] [小時(shí)] [天] [月] [周] [年]

例如:00:00:00?* 10,11,12 1#5 2018 ,表示2018年10月,11月和12月的第一周星期五的00:00:00。看上去不是很容易書寫與記憶,但是我們可以通過網(wǎng)絡(luò)上的在線Cron表達(dá)式生成工具,來幫助我們寫表達(dá)式:在線生成cron表達(dá)式的工具:http://cron.qqe2.com/

重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

特殊字符的含義如下:

星號( * ):可在所有字段中使用以指示相應(yīng)時(shí)域中的每次時(shí)間。例如,分鐘字段中的*表示“每分鐘”;

問號(?):此字符僅在日期和星期字段中使用。通常將其指定為“無意義的值”,等同于點(diǎn)字符;

減號(-):表示范圍。如果在小時(shí)字段中使用“ 10-12”,則表示10到12,即10、11、12;

逗號(,):表示列表值。如果在星期字段中使用“星期一,星期三,星期五”,則表示星期一,星期三和星期五;

斜線(/):X / Y表示相等的步長序列,其中X為起始值,y為增量步長值。如果在分鐘字段中使用0/15,則表示0、15、30和45秒,而5/15在分鐘字段中表示5、20、35、50,也可以使用* / y,這等效到0 / y;


quartz動態(tài)定時(shí)任務(wù)(數(shù)據(jù)庫持久化)前言

在項(xiàng)目開發(fā)過程當(dāng)中,某些定時(shí)任務(wù),可能在運(yùn)行一段時(shí)間之后,就不需要了,或者需要修改下定時(shí)任務(wù)的執(zhí)行時(shí)間等等。

需要在代碼當(dāng)中進(jìn)行修改然后重新打包發(fā)布,很麻煩。使用Quartz來實(shí)現(xiàn)的話不需要重新修改代碼而達(dá)到要求。


原理 使用quartz提供的API完成配置任務(wù)的增刪改查 將任務(wù)的配置保存在數(shù)據(jù)庫中


配置

application.yml

在上面已經(jīng)引入了maven依賴包,這里不再重復(fù)。直接spring屬性下面加入quartz配置信息

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

spring:  datasource:    url: jdbc:mysql://192.168.161.3:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false    username: test    password: 4rfv$RFV    driver-class-name: com.mysql.jdbc.Driver  quartz:    job-store-type: JDBC #數(shù)據(jù)庫存儲quartz任務(wù)配置    jdbc:      initialize-schema: NEVER #自動初始化表結(jié)構(gòu),第一次啟動的時(shí)候這里寫always
重學(xué)SpringBoot系列之異步任務(wù)與定時(shí)任務(wù)

動態(tài)配置代碼實(shí)現(xiàn)

第一步 創(chuàng)建一個(gè)定時(shí)任務(wù)相關(guān)實(shí)體類用于保存定時(shí)任務(wù)相關(guān)信息到數(shù)據(jù)庫當(dāng)中

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Datapublic class QuartzBean {    /** 任務(wù)id */    private String id;    /** 任務(wù)名稱 */    private String jobName;    /** 任務(wù)執(zhí)行類 */    private String jobClass;    /** 任務(wù)狀態(tài) 啟動還是暫停*/    private Integer status;    /** 任務(wù)運(yùn)行時(shí)間表達(dá)式 */    private String cronExpression;}

第二步 創(chuàng)建定時(shí)任務(wù)暫停,修改,啟動,單次啟動工具類

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

public class QuartzUtils {    /**     * 創(chuàng)建定時(shí)任務(wù) 定時(shí)任務(wù)創(chuàng)建之后默認(rèn)啟動狀態(tài)     * @param scheduler 調(diào)度器     * @param quartzBean 定時(shí)任務(wù)信息類     */    @SuppressWarnings("unchecked")    public static void createScheduleJob(Scheduler scheduler, QuartzBean quartzBean) throws ClassNotFoundException, SchedulerException {            //獲取到定時(shí)任務(wù)的執(zhí)行類 必須是類的絕對路徑名稱            //定時(shí)任務(wù)類需要是job類的具體實(shí)現(xiàn) QuartzJobBean是job的抽象類。            Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(quartzBean.getJobClass());            // 構(gòu)建定時(shí)任務(wù)信息            JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(quartzBean.getJobName()).build();            // 設(shè)置定時(shí)任務(wù)執(zhí)行方式            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExpression());            // 構(gòu)建觸發(fā)器trigger            CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(quartzBean.getJobName()).withSchedule(scheduleBuilder).build();            scheduler.scheduleJob(jobDetail, trigger);    }    /**     * 根據(jù)任務(wù)名稱暫停定時(shí)任務(wù)     * @param scheduler 調(diào)度器     * @param jobName 定時(shí)任務(wù)名稱     */    public static void pauseScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {        JobKey jobKey = JobKey.jobKey(jobName);        scheduler.pauseJob(jobKey);    }    /**     * 根據(jù)任務(wù)名稱恢復(fù)定時(shí)任務(wù)     * @param scheduler 調(diào)度器     * @param jobName 定時(shí)任務(wù)名稱     */    public static void resumeScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {        JobKey jobKey = JobKey.jobKey(jobName);        scheduler.resumeJob(jobKey);    }    /**     * 根據(jù)任務(wù)名稱立即運(yùn)行一次定時(shí)任務(wù)     * @param scheduler 調(diào)度器     * @param jobName 定時(shí)任務(wù)名稱     */    public static void runOnce(Scheduler scheduler, String jobName) throws SchedulerException {        JobKey jobKey = JobKey.jobKey(jobName);        scheduler.triggerJob(jobKey);    }    /**     * 更新定時(shí)任務(wù)     * @param scheduler 調(diào)度器     * @param quartzBean 定時(shí)任務(wù)信息類     */    public static void updateScheduleJob(Scheduler scheduler, QuartzBean quartzBean) throws SchedulerException {            //獲取到對應(yīng)任務(wù)的觸發(fā)器            TriggerKey triggerKey = TriggerKey.triggerKey(quartzBean.getJobName());            //設(shè)置定時(shí)任務(wù)執(zhí)行方式            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExpression());            //重新構(gòu)建任務(wù)的觸發(fā)器trigger            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();            //重置對應(yīng)的job            scheduler.rescheduleJob(triggerKey, trigger);    }    /**     * 根據(jù)定時(shí)任務(wù)名稱從調(diào)度器當(dāng)中刪除定時(shí)任務(wù)     * @param scheduler 調(diào)度器     * @param jobName 定時(shí)任務(wù)名稱     */    public static void deleteScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {        JobKey jobKey = JobKey.jobKey(jobName);        scheduler.deleteJob(jobKey);    }}

控制層

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

@Controller@RequestMapping("/quartz/job/")public class QuartzController {    //注入任務(wù)調(diào)度    @Resource    private Scheduler scheduler;    @PostMapping("/create")    @ResponseBody    public String createJob(@RequestBody QuartzBean quartzBean) throws SchedulerException, ClassNotFoundException {        QuartzUtils.createScheduleJob(scheduler,quartzBean);        return "已創(chuàng)建任務(wù)";//這里return不是生產(chǎn)級別代碼,測試簡單寫一下    }    @PostMapping("/pause")    @ResponseBody    public String pauseJob(String jobName) throws SchedulerException {        QuartzUtils.pauseScheduleJob (scheduler,jobName);        return "已暫停成功";//這里return不是生產(chǎn)級別代碼,測試簡單寫一下    }    @PostMapping("/run")    @ResponseBody    public String runOnce(String jobName) throws SchedulerException {        QuartzUtils.runOnce (scheduler,jobName);        return "運(yùn)行任務(wù)" + jobName + "成功";//這里return不是生產(chǎn)級別代碼,測試簡單寫一下    }    @PostMapping("/resume")    @ResponseBody    public String resume(String jobName) throws SchedulerException {        QuartzUtils.resumeScheduleJob(scheduler,jobName);        return "恢復(fù)定時(shí)任務(wù)成功:" + jobName;    }    @PostMapping("/update")    @ResponseBody    public String update(@RequestBody QuartzBean quartzBean) throws SchedulerException {        QuartzUtils.updateScheduleJob(scheduler,quartzBean);        return "更新定時(shí)任務(wù)調(diào)度信息成功";    }}

分布式任務(wù)調(diào)度框架—xxl-job

官網(wǎng)文檔

全新版XXL-JOB分布式定時(shí)框架SrpingBoot-XXL-JOB

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊5 分享