上兩章節,咱們簡單的講解了關於異步調用和異步請求相關知識點。這一章節,咱們來說講開發過程也是常常會遇見的定時任務。好比天天定時清理無效數據、定時發送短信、定時發送郵件、支付系統中的定時對帳等等,每每都會定義一些定時器,進行此業務的開發。因此,本章節介紹下在
SpringBoot
中定時任務如何使用及一點分佈式定時服務的思考總結。html
在JAVA
開發領域,目前能夠經過如下幾種方式進行定時任務:java
Spring
提供的一個任務調度工具,支持註解和配置文件形式,支持Cron
表達式,使用簡單但功能強大。題外話:對於Quartz
,早前用過1.6版本的,更新到2.x及以上版本後基本沒怎麼接觸了,原來還有倒騰過結合Kettle
作了一些動態的定時抽取數據啥的還編寫過一個Cron
表達式編輯器,如今基本忘記了。。等有機會,再次深刻學習後再來單獨分享一些關於的Quartz
心得吧。git
剛剛有介紹過,基於
JDK
方式一共有兩種:Timer
和ScheduledExecutorService
。接下來,就簡單講解下這兩種方式。github
Timer
是jdk提供的java.util.Timer
類。redis
簡單示例:spring
@GetMapping("/timer") public String doTimer() { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { log.info("Timer定時任務啓動:" + new Date()); } }, 1000,1000);//延遲1秒啓動,每1秒執行一次 return "timer";
啓動後,訪問便可看見控制檯週期性輸出信息了:數據庫
2018-08-18 21:30:35.171 INFO 13352 --- [ Timer-0] c.l.l.s.c.controller.TaskController : Timer定時任務啓動:Sat Aug 18 21:30:35 CST 2018 2018-08-18 21:30:36.173 INFO 13352 --- [ Timer-0] c.l.l.s.c.controller.TaskController : Timer定時任務啓動:Sat Aug 18 21:30:36 CST 2018 2018-08-18 21:30:37.173 INFO 13352 --- [ Timer-0] c.l.l.s.c.controller.TaskController : Timer定時任務啓動:Sat Aug 18 21:30:37 CST 2018 2018-08-18 21:30:38.173 INFO 13352 --- [ Timer-0] c.l.l.s.c.controller.TaskController : Timer定時任務啓動:Sat Aug 18 21:30:38 CST 2018 2018-08-18 21:30:39.174 INFO 13352 --- [ Timer-0] c.l.l.s.c.controller.TaskController : Timer定時任務啓動:Sat Aug 18 21:30:39 CST 2018 ......
相關API簡單說明:編程
一、在特定時間執行任務,只執行一次api
public void schedule(TimerTask task,Date time)
二、在特定時間以後執行任務,只執行一次springboot
public void schedule(TimerTask task,long delay)
三、指定第一次執行的時間,而後按照間隔時間,重複執行
public void schedule(TimerTask task,Date firstTime,long period)
四、在特定延遲以後第一次執行,而後按照間隔時間,重複執行
public void schedule(TimerTask task,long delay,long period)
五、第一次執行以後,特定頻率執行,與3同
public void scheduleAtFixedRate(TimerTask task,Date firstTime,long period)
六、在delay毫秒以後第一次執行,後按照特定頻率執行
public void scheduleAtFixedRate(TimerTask task,long delay,long period)
參數:
取消任務使用:timer.cancel()
方法便可註銷任務。
此類相對用的較少了,簡單瞭解下。
ScheduledExecutorService
能夠說是Timer
的替代類,由於Timer
不支持多線程,任務是串行的,並且也不捕獲異常,假設某個任務異常了,整個Timer
就沒法運行了。
簡單示例:
@GetMapping("/executor") public String ScheduledExecutorService() { // ScheduledExecutorService service = Executors.newScheduledThreadPool(10); service.scheduleAtFixedRate(new Runnable() { @Override public void run() { log.info("ScheduledExecutorService定時任務執行:" + new Date()); } }, 1, 1, TimeUnit.SECONDS);//首次延遲1秒,以後每1秒執行一次 log.info("ScheduledExecutorService定時任務啓動:" + new Date()); return "ScheduledExecutorService!"; }
啓動後,可看見控制檯按設定的頻率輸出:
2018-08-18 22:03:24.840 INFO 6752 --- [nio-8080-exec-1] c.l.l.s.c.controller.TaskController : ScheduledExecutorService定時任務啓動:Sat Aug 18 22:03:24 CST 2018 2018-08-18 22:03:25.841 INFO 6752 --- [pool-1-thread-1] c.l.l.s.c.controller.TaskController : ScheduledExecutorService定時任務執行:Sat Aug 18 22:03:25 CST 2018 2018-08-18 22:03:26.842 INFO 6752 --- [pool-1-thread-1] c.l.l.s.c.controller.TaskController : ScheduledExecutorService定時任務執行:Sat Aug 18 22:03:26 CST 2018 2018-08-18 22:03:27.841 INFO 6752 --- [pool-1-thread-2] c.l.l.s.c.controller.TaskController : ScheduledExecutorService定時任務執行:Sat Aug 18 22:03:27 CST 2018 2018-08-18 22:03:28.840 INFO 6752 --- [pool-1-thread-1] c.l.l.s.c.controller.TaskController : ScheduledExecutorService定時任務執行:Sat Aug 18 22:03:28 CST 2018 2018-08-18 22:03:29.840 INFO 6752 --- [pool-1-thread-3] c.l.l.s.c.controller.TaskController : ScheduledExecutorService定時任務執行:Sat Aug 18 22:03:29 CST 2018
可同時設置多個任務,只需再次設置scheduleAtFixedRate
便可。
經常使用方法說明:
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
參數說明: 0. command:執行線程
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);
參數說明: 0. command:執行線程
其餘的方法你們可自行谷歌下。
使用
SpringTask
在SpringBoot
是很簡單的,使用@Scheduled
註解便可輕鬆搞定。
0.啓動類,加入@EnableScheduling
讓註解@Scheduled
生效。
@SpringBootApplication @EnableScheduling @Slf4j public class Chapter22Application { public static void main(String[] args) { SpringApplication.run(Chapter22Application.class, args); log.info("Chapter22啓動!"); } }
1.編寫一個調度類,系統啓動後自動掃描,自動執行。
@Component @Slf4j public class ScheduledTask { /** * 自動掃描,啓動時間點以後5秒執行一次 */ @Scheduled(fixedRate=5000) public void getCurrentDate() { log.info("Scheduled定時任務執行:" + new Date()); } }
2.啓動後,控制檯可就看見每5秒一次輸出了:
2018-08-18 22:23:09.735 INFO 13812 --- [pool-1-thread-1] c.l.l.s.c.controller.ScheduledTask : Scheduled定時任務執行:Sat Aug 18 22:23:09 CST 2018 2018-08-18 22:23:14.734 INFO 13812 --- [pool-1-thread-1] c.l.l.s.c.controller.ScheduledTask : Scheduled定時任務執行:Sat Aug 18 22:23:14 CST 2018 2018-08-18 22:23:19.735 INFO 13812 --- [pool-1-thread-1] c.l.l.s.c.controller.ScheduledTask : Scheduled定時任務執行:Sat Aug 18 22:23:19 CST 2018 2018-08-18 22:23:24.735 INFO 13812 --- [pool-1-thread-1] c.l.l.s.c.controller.ScheduledTask : Scheduled定時任務執行:Sat Aug 18 22:23:24 CST 2018 2018-08-18 22:23:29.735 INFO 13812 --- [pool-1-thread-1] c.l.l.s.c.controller.ScheduledTask : Scheduled定時任務執行:Sat Aug 18 22:23:29 CST 2018 ......
使用都是簡單的,如今咱們來看看註解@Scheduled
的參數意思:
initialDelay
, 定義該任務延遲執行時間。一個
cron
表達式有至少6個(也可能7個)有空格分隔的時間元素。
依次順序以下表所示:
字段 | 容許值 | 容許的特殊字符 |
---|---|---|
秒 | 0~59 | , - * / |
分 | 0~59 | , - * / |
小時 | 0~23 | , - * / |
日期 | 1-31 | , - * ? / L W C |
月份 | 1~12或者JAN~DEC | , - * / |
星期 | 1~7或者SUN~SAT | , - * ? / L C # |
年(可選) | 留空,1970~2099 | , - * / |
簡單舉例:
更多表達式,可訪問:http://cron.qqe2.com/ 進行在線表達式編寫。簡單明瞭。
從控制檯輸出能夠看見,多任務使用的是同一個線程。可結合上章節的異步調用來實現不一樣任務使用不一樣的線程進行任務執行。
0.編寫配置類,同時啓用@Async
註解:
@Configuration @EnableAsync public class Config { /** * 配置線程池 * @return */ @Bean(name = "scheduledPoolTaskExecutor") public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("oKong-Scheduled-"); // 線程池對拒絕任務(無線程可用)的處理策略,目前只支持AbortPolicy、CallerRunsPolicy;默認爲後者 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //調度器shutdown被調用時等待當前被調度的任務完成 taskExecutor.setWaitForTasksToCompleteOnShutdown(true); //等待時長 taskExecutor.setAwaitTerminationSeconds(60); taskExecutor.initialize(); return taskExecutor; } }
1.調度類上加入@Async
。
@Component @Slf4j public class ScheduledTask { /** * 自動掃描,啓動時間點以後5秒執行一次 */ @Async("scheduledPoolTaskExecutor") @Scheduled(fixedRate=5000) public void getCurrentDate() { log.info("Scheduled定時任務執行:" + new Date()); } }
再次啓動程序,可看見控制檯輸出,任務已是不一樣線程下執行了:
2018-08-18 22:47:13.313 INFO 14212 --- [ong-Scheduled-1] c.l.l.s.c.controller.ScheduledTask : Scheduled定時任務執行:Sat Aug 18 22:47:13 CST 2018 2018-08-18 22:47:13.343 INFO 14212 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) 2018-08-18 22:47:13.348 INFO 14212 --- [ main] c.l.l.s.chapter22.Chapter22Application : Started Chapter22Application in 2.057 seconds (JVM running for 2.855) 2018-08-18 22:47:13.348 INFO 14212 --- [ main] c.l.l.s.chapter22.Chapter22Application : Chapter22啓動! 2018-08-18 22:47:18.308 INFO 14212 --- [ong-Scheduled-2] c.l.l.s.c.controller.ScheduledTask : Scheduled定時任務執行:Sat Aug 18 22:47:18 CST 2018
使用註解的方式,沒法實現動態的修改或者添加新的定時任務的,這個使用就須要使用編程的方式進行任務的更新操做了。可直接使用
ThreadPoolTaskScheduler
或者SchedulingConfigurer
接口進行自定義定時任務建立。
ThreadPoolTaskScheduler
是SpringTask
的核心實現類,該類提供了大量的重載方法進行任務調度。這裏簡單示例下,具體的你們自行搜索下,用的少不太瞭解呀。
0.建立一個ThreadPoolTaskScheduler
類。
@Bean("taskExecutor") public TaskScheduler taskExecutor() { ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler(); executor.setPoolSize(20); executor.setThreadNamePrefix("oKong-taskExecutor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //調度器shutdown被調用時等待當前被調度的任務完成 executor.setWaitForTasksToCompleteOnShutdown(true); //等待時長 executor.setAwaitTerminationSeconds(60); return executor; }
1.編寫一個控制類,動態設置定時任務:
@Autowired TaskScheduler taskScheduler; @GetMapping("/poolTask") public String threadPoolTaskScheduler() { taskScheduler.schedule(new Runnable() { @Override public void run() { log.info("ThreadPoolTaskScheduler定時任務:" + new Date()); } }, new CronTrigger("0/3 * * * * ?"));//每3秒執行一次 return "ThreadPoolTaskScheduler!"; }
2.啓動後,訪問接口,便可看見控制檯每3秒輸出一次:
2018-08-18 23:20:39.002 INFO 9120 --- [Kong-Executor-1] c.l.l.s.c.controller.TaskController : ThreadPoolTaskScheduler定時任務:Sat Aug 18 23:20:39 CST 2018 2018-08-18 23:20:42.000 INFO 9120 --- [Kong-Executor-1] c.l.l.s.c.controller.TaskController : ThreadPoolTaskScheduler定時任務:Sat Aug 18 23:20:42 CST 2018 2018-08-18 23:20:45.002 INFO 9120 --- [Kong-Executor-2] c.l.l.s.c.controller.TaskController : ThreadPoolTaskScheduler定時任務:Sat Aug 18 23:20:45 CST 2018 2018-08-18 23:20:48.001 INFO 9120 --- [Kong-Executor-1] c.l.l.s.c.controller.TaskController : ThreadPoolTaskScheduler定時任務:Sat Aug 18 23:20:48 CST 2018
此類是個接口,直接實現其
configurerTasks
方法便可。
0.編寫配置類:
@Configuration @Slf4j public class ScheduleConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setTaskScheduler(taskExecutor()); taskRegistrar.getScheduler().schedule(new Runnable() { @Override public void run() { log.info("SchedulingConfigurer定時任務:" + new Date()); } }, new CronTrigger("0/3 * * * * ?"));//每3秒執行一次 } @Bean("taskExecutor") public TaskScheduler taskExecutor() { ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler(); executor.setPoolSize(20); executor.setThreadNamePrefix("oKong-Executor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //調度器shutdown被調用時等待當前被調度的任務完成 executor.setWaitForTasksToCompleteOnShutdown(true); //等待時長 executor.setAwaitTerminationSeconds(60); return executor; } }
1.啓動後,控制檯也能夠看見每3秒輸出一次:
2018-08-18 23:24:39.001 INFO 868 --- [Kong-Executor-1] c.l.l.s.chapter22.config.ScheduleConfig : SchedulingConfigurer定時任務:Sat Aug 18 23:24:39 CST 2018 2018-08-18 23:24:42.001 INFO 868 --- [Kong-Executor-1] c.l.l.s.chapter22.config.ScheduleConfig : SchedulingConfigurer定時任務:Sat Aug 18 23:24:42 CST 2018 2018-08-18 23:24:45.000 INFO 868 --- [Kong-Executor-2] c.l.l.s.chapter22.config.ScheduleConfig : SchedulingConfigurer定時任務:Sat Aug 18 23:24:45 CST 2018
因爲本章節是基於
SpringBoot 1.x
版本的,因此沒有基於Quartz
的starter
配置,這裏直接引入了Quartz
相關依賴包來集成。
題外話:本來使用SpringMvc
時,通常上都是經過xml
文件,配置其org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean
類進行具體執行任務的配置,指定執行的對象和方法。而後經過設置CronTriggerFactoryBean
或者SimpleTriggerFactoryBean
設置定時器,最後經過org.springframework.scheduling.quartz.SchedulerFactoryBean
加入調度的trigger
。因此,咱們就使用javaConfig
方式進行簡單集成下。
0.加入pom依賴
<!-- quartz --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.3</version> </dependency> <!-- spring集成quartz --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <!-- 由於SchedulerFactoryBean中依賴了org.springframework.transaction.PlatformTransactionManager,因此需依賴tx相關包,其實仍是quartz有個分佈式功能,是使用數據庫完成的。 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency>
1.編寫配置類。
@Configuration @Slf4j public class QuartzConfig { /** * 經過工廠類,建立job實例 * @return */ @Bean public MethodInvokingJobDetailFactoryBean customJobDetailFactoryBean() { MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean(); //設置執行任務的bean jobDetail.setTargetBeanName("quartzTask"); //設置具體執行的方法 jobDetail.setTargetMethod("quartzTask"); //同步執行,上一任務未執行完,下一任務等待 //true 任務併發執行 //false 下一個任務必須等待上一任務完成 jobDetail.setConcurrent(false); return jobDetail; } /** * 經過工廠類建立Trigger * @param jobDetailFactoryBean * @return * @throws ParseException */ @Bean(name = "cronTriggerBean") public Trigger cronTriggerBean(MethodInvokingJobDetailFactoryBean jobDetailFactoryBean) throws ParseException { CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); cronTriggerFactoryBean.setJobDetail(jobDetailFactoryBean.getObject()); cronTriggerFactoryBean.setCronExpression("0/3 * * * * ?");//每3秒執行一次 cronTriggerFactoryBean.setName("customCronTrigger"); cronTriggerFactoryBean.afterPropertiesSet(); return cronTriggerFactoryBean.getObject(); } /** * 調度工廠類,自動注入Trigger * @return */ @Bean public SchedulerFactoryBean schedulerFactoryBean(Trigger... triggers) { SchedulerFactoryBean bean = new SchedulerFactoryBean(); //也能夠直接注入 ApplicationContext,利於 getBeansOfType獲取trigger // Map<String,Trigger> triggerMap = appContext.getBeansOfType(Trigger.class); // if(triggerMap != null) { // List<Trigger> triggers = new ArrayList<>(triggerMap.size()); // // // triggerMap.forEach((key,trigger)->{ // triggers.add(trigger); // }); // bean.setTriggers(triggers.toArray(new Trigger[triggers.size()])); // } //這裏注意 對應的trigger 不能爲null 否則會異常的 bean.setTriggers(triggers); return bean; } @Component("quartzTask") public class QuartzTask { public void quartzTask() { log.info("Quartz定時任務:" + new Date()); } } }
2.啓動後,能夠看見控制檯以每3秒執行一次輸出:
2018-08-18 23:42:03.019 INFO 772 --- [ryBean_Worker-2] c.l.l.s.chapter22.config.QuartzConfig : Quartz定時任務:Sun Aug 18 23:42:03 CST 2018 2018-08-18 23:42:06.002 INFO 772 --- [ryBean_Worker-3] c.l.l.s.chapter22.config.QuartzConfig : Quartz定時任務:Sun Aug 18 23:42:06 CST 2018 2018-08-18 23:42:09.002 INFO 772 --- [ryBean_Worker-4] c.l.l.s.chapter22.config.QuartzConfig : Quartz定時任務:Sun Aug 18 23:42:09 CST 2018
關於Quartz
的詳細用法,再次不表了。很久沒有使用過了。有機會再來詳細闡述吧。
在單機模式下,定時任務是沒什麼問題的。但當咱們部署了多臺服務,同時又每臺服務又有定時任務時,若不進行合理的控制在同一時間,只有一個定時任務啓動執行,這時,定時執行的結果就可能存在混亂和錯誤了。
這裏簡單的說說相關的解決方案吧,一家之言,但願你們能提出本身的看法,共同進步!
Quartz
集羣方案:自己Quartz
是支持經過數據庫實現集羣的,如下是其集羣架構圖:其實現原理也相對簡單:經過數據庫實現任務的持久化,保存定時任務的相關配置信息,以保證下次系統啓動時,定時任務能自動啓動。同時,經過數據庫行鎖(for update)
機制,控制一個任務只能被一個實例運行,只有獲取鎖的實例才能運行任務,其餘的只能等待,直到鎖被釋放。這種方式有些弊端,就是依賴了數據庫,同時也須要保證各服務器之間的時間須要同步,否則也是會混亂的。
如今Quartz
也有基於Redis
的集羣方案,有興趣的能夠搜索下。
分佈式鎖:可經過使用Redis
或者ZooKeeper
實現一個分佈式鎖的機制,使得只有獲取到鎖的實例方能運行定時任務,避免任務重複執行。可查看下開源的基於Redis
實現的分佈式鎖項目:redisson
。github地址:https://github.com/redisson/redisson有興趣的同窗能夠了解下。
統一調度中心:
可構建一個純粹
的定時服務,只有定時器
相關配置,好比定時時間
,定時調度的api接口
或者http
服務,甚至是統一註冊中心下的服務類,如dubbo服務等。而具體的任務執行操做都在各自業務方系統中,調度中心只負責接口的調用
,具體實現仍是在業務方。這種方案相對來講比較通用,實現起來也簡單。就是須要業務方進行約定編程,或者對外提供一個api接口。
固然,爲了實現定時任務的自動發現和註冊功能,仍是須要規範一套規則來實現自動註冊功能。簡單來講,以Dubbo
服務爲例,能夠定義一個定時任務接口類
,調度中心只須要獲取全部實現此接口的服務,同時經過服務的相關配置(調度時間、失敗策略等)進行相關定時操做。或者編寫一個服務註冊與發現的客戶端,經過Spring
獲取到實現此接口的全部實現類,上送到調度中心。
並且,統一調度中心,還能夠對全部的定時任務的調度狀況進行有效監控,日誌記錄等,也能夠約定接口,讓定時任務回傳定時結果,作到全局把控的目的。
以上就是對分佈式調度的一點理解,有錯誤的地方還望指正,有更好的方案也但願能分享下。
本章節主要是講解了經過不一樣的方式實現定時任務。對於定時任務而言,自己是門大學問,一倆篇文章是講不完的。像
SpringTask
和Quartz
都是很強大的調度器,二者很類似,像如何實現任務的動態修改調度週期,動態中止相關任務,調度任務的監控,這些本文章都沒有涉及。還但願有相關需求的同窗自行搜索相關資料了。
目前互聯網上不少大佬都有
SpringBoot
系列教程,若有雷同,請多多包涵了。本文是做者在電腦前一字一句敲的,每一步都是本身實踐的。若文中有所錯誤之處,還望提出,謝謝。
499452441
lqdevOps
我的博客:http://blog.lqdev.cn
完整示例:chapter-22
原文地址:http://blog.lqdev.cn/2018/08/19/springboot/chapter-twenty-two/