一文帶你吃透定時(shí)任務(wù)框架
一、介紹
說(shuō)到定時(shí)任務(wù),相信大家都不陌生,在我們實(shí)際的工作中,用到定時(shí)任務(wù)的場(chǎng)景可以說(shuō)非常的多,例如:
- 雙 11 的 0 點(diǎn),定時(shí)開(kāi)啟秒殺
- 每月1號(hào),財(cái)務(wù)系統(tǒng)自動(dòng)拉取每個(gè)人的績(jī)效工資,用于薪資計(jì)算
- 使用 TCP 長(zhǎng)連接時(shí),客戶(hù)端按照固定頻率定時(shí)向服務(wù)端發(fā)送心跳請(qǐng)求
等等,定時(shí)器像水和空氣一般,普遍存在于各個(gè)場(chǎng)景中,在實(shí)際的業(yè)務(wù)開(kāi)發(fā)中,基本上少不了定時(shí)任務(wù)的應(yīng)用。
總結(jié)起來(lái),一般定時(shí)任務(wù)的表現(xiàn)有以下幾個(gè)特征:
1.在某個(gè)時(shí)刻觸發(fā),例如11.11號(hào)0點(diǎn)開(kāi)啟秒殺
2.按照固定頻率周期性觸發(fā),例如每分鐘發(fā)送心跳請(qǐng)求
3.預(yù)約指定時(shí)刻開(kāi)始周期性觸發(fā),例如從12.1號(hào)開(kāi)始每天7點(diǎn)鬧鐘響起
說(shuō)了這么多,也不BB了,下面我們就來(lái)點(diǎn)干活,列舉一些實(shí)際項(xiàng)目中使用的相關(guān)工具!
二、crontab 定時(shí)器
2.1、介紹
crontab 嚴(yán)格來(lái)說(shuō)并不是屬于 java 內(nèi)的,它是 linux 自帶的一個(gè)工具,可以周期性地執(zhí)行某個(gè)shell腳本或命令。
由于 crontab 在實(shí)際開(kāi)發(fā)中應(yīng)用比較多, 特別是對(duì)于運(yùn)維的人,crontab 命令是必須用到的命令,自動(dòng)化運(yùn)維中一定少不了它,而且 crontab 表達(dá)式跟我們后面要介紹的其他定時(shí)任務(wù)框架,例如 Quartz,Spring Schedule 的 cron 表達(dá)式類(lèi)似,所以這里先介紹 crontab。
簡(jiǎn)而言之,crontab 就是一個(gè)自定義定時(shí)器。
命令格式如下:
.---------------- minute (0 - 59)
| .------------- hour (0 - 23)
| | .---------- day of month (1 - 31)
| | | .------- month (1 - 12) OR jan,feb,mar,apr ...
| | | | .---- day of week (0 - 6) (Sunday=0 or 7) ...
| | | | |
* * * * * command
- 第一個(gè)參數(shù)(minute):代表一小時(shí)內(nèi)的第幾分,范圍 0-59。
- 第二個(gè)參數(shù)(hour):代表一天中的第幾小時(shí),范圍 0-23。
- 第三個(gè)參數(shù)(day):代表一個(gè)月中的第幾天,范圍 1-31。
- 第四個(gè)參數(shù)(month):代表一年中第幾個(gè)月,范圍 1-12。
- 第五個(gè)參數(shù)(week):代表星期幾,范圍 0-7 (0及7都是星期天)。
- 第六個(gè)參數(shù)(command):所要執(zhí)行的指令。
具體應(yīng)用的時(shí)候,時(shí)間定義大概是長(zhǎng)下面這個(gè)樣子!
# 每5分鐘執(zhí)行一次命令
*/5 * * * * Command
# 每小時(shí)的第5分鐘執(zhí)行一次命令
5 * * * * Command
# 指定每天下午的 6:30 執(zhí)行一次命令
30 18 * * * Command
# 指定每月8號(hào)的7:30分執(zhí)行一次命令
30 7 8 * * Command
# 指定每年的6月8日5:30執(zhí)行一次命令
30 5 8 6 * Command
# 指定每星期日的6:30執(zhí)行一次命令
30 6 * * 0 Command
2.2、具體示例
以centOS操作系統(tǒng)為例,創(chuàng)建一個(gè)定時(shí)任務(wù),每分鐘執(zhí)行某個(gè)指定shell腳本,過(guò)程如下!
- 首先安裝 crond 相關(guān)服務(wù)
yum -y install cronie yum-cron
- 編寫(xiě)一個(gè)輸出當(dāng)前時(shí)間到日志的shell腳本
#創(chuàng)建一個(gè)test.sh腳本
vim /root/shell/test.sh
#腳本內(nèi)容如下,將內(nèi)容輸出到file.log文件
echo `date '+%Y-%m-%d %H:%M:%S'` >> /root/shell/file.log
- 先執(zhí)行一下腳本,觀察內(nèi)容是否輸出到file.log文件
sh /root/shell/test.sh
- 查看日志文件內(nèi)容
cat /root/shell/file.log
如果出現(xiàn)以下內(nèi)容,說(shuō)明運(yùn)行正常!
圖片
- 接著再來(lái)創(chuàng)建一個(gè)定時(shí)任務(wù),每分鐘執(zhí)行一次test.sh
#編輯定時(shí)任務(wù)【刪除-添加-修改】
crontab -e
- 在文件末尾加入如下信息
#每分鐘執(zhí)行一次test.sh腳本
*/1 * * * * sh /root/shell/test.sh
- 如果想查定時(shí)任務(wù)是否加入,可以通過(guò)如下命令
#查看crontab定時(shí)任務(wù)
crontab -l
圖片
- 最后就是重啟定時(shí)任務(wù)
##重新載入配置
systemctl reload crond
#重啟服務(wù)
systemctl restart crond
- 查看file.log文件實(shí)時(shí)輸出內(nèi)容,觀察test.sh腳本是否被執(zhí)行
tail -f /root/shell/file.log
從結(jié)果上看,運(yùn)行正常!
圖片
- 如果想查看定時(shí)任務(wù)日志,可通過(guò)如下命令進(jìn)行查看
tail -f /var/log/cron
如果想移除某個(gè)定時(shí)任務(wù),直接輸入crontab -e命令,并移除對(duì)應(yīng)的腳本,然后刷新配置、重啟服務(wù)即可!
如果想深入的了解crontab使用,可以參考這篇文章,里面總結(jié)得比較好, 這里就不再多說(shuō)了。
三、java 自帶的定時(shí)器
3.1、Timer
Timer 定時(shí)器,由 jdk 提供的java.util.Timer和java.util.TimerTask兩個(gè)類(lèi)組合實(shí)現(xiàn)。
其中TimerTask表示某個(gè)具體任務(wù),而Timer則是進(jìn)行調(diào)度任務(wù)處理。
實(shí)現(xiàn)過(guò)程也很簡(jiǎn)單,示例如下:
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest extends TimerTask {
private String jobName;
public TimerTest(String jobName) {
this.jobName = jobName;
}
@Override
public void run() {
System.out.println("execute " + jobName);
}
public static void main(String[] args) {
Timer timer = new Timer();
long delay1 = 1 * 1000;
long period1 = 1 * 1000;
// 從現(xiàn)在開(kāi)始 1 秒鐘之后,每隔 1 秒鐘執(zhí)行一次 job1
timer.schedule(new TimerTest("job1"), delay1, period1);
long delay2 = 2 * 1000;
long period2 = 2 * 1000;
// 從現(xiàn)在開(kāi)始 2 秒鐘之后,每隔 2 秒鐘執(zhí)行一次 job2
timer.schedule(new TimerTest("job2"), delay2, period2);
}
}
輸出結(jié)果:
execute job1
execute job2
execute job1
execute job1
execute job2
execute job1
execute job1
execute job2
...
Timer 的優(yōu)點(diǎn)在于簡(jiǎn)單易用,由于所有任務(wù)都是由同一個(gè)線程來(lái)調(diào)度,因此所有任務(wù)都是串行執(zhí)行的,同一時(shí)間只能有一個(gè)任務(wù)在執(zhí)行,前一個(gè)任務(wù)的延遲或異常都將會(huì)影響到之后的任務(wù)。
具體原因如下:
- 1.當(dāng)一個(gè)線程拋出異常時(shí),整個(gè) Timer 都會(huì)停止運(yùn)行。例如上面的 job1 拋出異常的話,job2 也不會(huì)再跑了
- 當(dāng)一個(gè)線程里面處理的時(shí)間非常長(zhǎng)的時(shí)候,會(huì)影響其他 job 的調(diào)度。例如,如果 job1 處理的時(shí)間要 1 分鐘, 那么 job2 至少要等 1 分鐘之后才能跑。
基于上面的原因,Timer 現(xiàn)在生產(chǎn)環(huán)境中都不在使用!
3.2、ScheduledExecutor
鑒于 Timer 的上述缺陷,從 Java 5 開(kāi)始,推出了基于線程池設(shè)計(jì)的 ScheduledExecutor。
其設(shè)計(jì)思想是,每一個(gè)被調(diào)度的任務(wù)都會(huì)由線程池中一個(gè)線程來(lái)管理執(zhí)行,因此任務(wù)是并發(fā)執(zhí)行的,相互之間不會(huì)受到干擾。需要注意的是,只有當(dāng)任務(wù)的執(zhí)行時(shí)間到來(lái)時(shí),ScheduedExecutor 才會(huì)真正啟動(dòng)一個(gè)線程,其余時(shí)間 ScheduledExecutor 都是在輪詢(xún)?nèi)蝿?wù)的狀態(tài)。
實(shí)現(xiàn)過(guò)程,示例如下:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorTest implements Runnable {
private String jobName;
public ScheduledExecutorTest(String jobName) {
this.jobName = jobName;
}
@Override
public void run() {
System.out.println("execute " + jobName);
}
public static void main(String[] args) {
//設(shè)置10個(gè)核心線程
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
long initialDelay1 = 1;
long period1 = 1;
// 從現(xiàn)在開(kāi)始1秒鐘之后,每隔1秒鐘執(zhí)行一次job1
service.scheduleAtFixedRate(
new ScheduledExecutorTest("job1"), initialDelay1,
period1, TimeUnit.SECONDS);
long initialDelay2 = 2;
long delay2 = 2;
// 從現(xiàn)在開(kāi)始2秒鐘之后,每隔2秒鐘執(zhí)行一次job2
service.scheduleWithFixedDelay(
new ScheduledExecutorTest("job2"), initialDelay2,
delay2, TimeUnit.SECONDS);
}
}
輸出結(jié)果:
execute job1
execute job1
execute job2
execute job1
execute job1
execute job2
execute job1
在 ScheduledExecutorService 中,由initialDelay、delay和TimeUnit三個(gè)參數(shù)決定任務(wù)的執(zhí)行頻率。
其中:
- TimeUnit:表示執(zhí)行的單位,例如:毫秒、秒、分、小時(shí)、天...
- initialDelay:表示從現(xiàn)在開(kāi)始多少TimeUnit后執(zhí)行任務(wù)
- delay:表示任務(wù)執(zhí)行周期,每隔多少TimeUnit執(zhí)行一次任務(wù)
從 api 上可以看到,ScheduledExecutorService 的出現(xiàn),完全可以替代 Timer ,同時(shí)完美的解決上面所說(shuō)的 Timer 存在的兩個(gè)問(wèn)題!
- 當(dāng)任務(wù)拋異常時(shí),即使異常沒(méi)有被捕獲, 線程池也還會(huì)新建線程,所以定時(shí)任務(wù)不會(huì)停止
- 由于 ScheduledExecutorService 是不同線程處理不同的任務(wù),因此不管一個(gè)線程的運(yùn)行時(shí)間有多長(zhǎng),都不會(huì)影響到另外一個(gè)線程的運(yùn)行
但是 ScheduledExecutorService 也不是萬(wàn)能的,比如我想每月1號(hào)統(tǒng)計(jì)一次報(bào)表、每季度月末統(tǒng)計(jì)銷(xiāo)售額等等這樣的需求。
你會(huì)發(fā)現(xiàn)使用 ScheduledExecutorService 實(shí)現(xiàn)的時(shí)候,每次任務(wù)執(zhí)行之后,你需要從當(dāng)前時(shí)間開(kāi)始出下一次執(zhí)行時(shí)間的間隔,而且每次都要重算,非常麻煩!
遇到這樣的需求,就需要一個(gè)更加完善的任務(wù)調(diào)度框架來(lái)解決這些復(fù)雜的調(diào)度問(wèn)題。
而我們所熟悉的開(kāi)源框架 Quartz 在這方面就提供了強(qiáng)大的支持。
四、第三方定時(shí)器
4.1、Quartz
quartz 在 java 項(xiàng)目中應(yīng)用非常的廣,市面上很多的開(kāi)源調(diào)度框架也基本都是直接或間接基于這個(gè)框架來(lái)開(kāi)發(fā)的。
下面我們就通過(guò)一個(gè)例子,來(lái)簡(jiǎn)單地認(rèn)識(shí)一下它。
- 引入quartz包
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
- 編寫(xiě)示例代碼
public class QuartzTest implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
public static void main(String[] args) throws SchedulerException {
// 創(chuàng)建一個(gè)Scheduler
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 啟動(dòng)Scheduler
scheduler.start();
// 新建一個(gè)Job, 指定執(zhí)行類(lèi)是QuartzTest, 指定一個(gè)K/V類(lèi)型的數(shù)據(jù), 指定job的name和group
JobDetail job = JobBuilder.newJob(QuartzTest.class)
.usingJobData("jobData", "test")
.withIdentity("myJob", "myJobGroup")
.build();
// 新建一個(gè)Trigger, 表示JobDetail的調(diào)度計(jì)劃, 這里的cron表達(dá)式是 每1秒執(zhí)行一次
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "myTriggerGroup")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
.build();
// 讓scheduler開(kāi)始調(diào)度這個(gè)job, 按trigger指定的計(jì)劃
scheduler.scheduleJob(job, trigger);
}
}
輸出結(jié)果:
2020-11-09 21:38:40
2020-11-09 21:38:45
2020-11-09 21:38:50
2020-11-09 21:38:55
2020-11-09 21:39:00
2020-11-09 21:39:05
2020-11-09 21:39:10
...
當(dāng)然,你還可以從JobExecutionContext對(duì)象中,獲取上文usingJobData()方法中設(shè)置的值。
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
//從context中獲取instName,groupName以及dataMap
String jobName = context.getJobDetail().getKey().getName();
String groupName = context.getJobDetail().getKey().getGroup();
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
//從dataMap中獲取myDescription,myValue以及myArray
String value = dataMap.getString("jobData");
System.out.println("jobName:" + jobName + ",groupName:" + groupName + ",jobData:" + value);
}
輸出結(jié)果:
jobName:myJob,groupName:myJobGroup,jobData:test
在 Quartz 工具包中,設(shè)計(jì)的核心類(lèi)主要包括 Scheduler, Job 以及 Trigger。
- Scheduler:可以理解為是一個(gè)具體的調(diào)度實(shí)例,用來(lái)調(diào)度任務(wù)
- JobDetail:定義具體作業(yè)的實(shí)例,進(jìn)一步封裝和拓展了 Job 功能,其中 Job 是一個(gè)接口,類(lèi)似上面的 TimerTask
- Trigger:設(shè)置任務(wù)調(diào)度策略。例如多久執(zhí)行一次,什么時(shí)候執(zhí)行,以什么頻率執(zhí)行等等
相比 JDK 提供的任務(wù)調(diào)度服務(wù),Quartz 最明顯的一個(gè)特點(diǎn)就是將任務(wù)調(diào)度者、任務(wù)具體實(shí)例、任務(wù)調(diào)度策略進(jìn)行三方解耦,這么做的優(yōu)點(diǎn)在于同一個(gè) Job 可以綁定多個(gè)不同的 Trigger,同一個(gè) Trigger 也可以調(diào)度多個(gè) Job,配置靈活性非常強(qiáng)。
Trigger 同時(shí)還支持cron表達(dá)式,在任務(wù)調(diào)度時(shí)間配置方面,更加靈活。
當(dāng)然,Quartz 的用途不僅僅在單例服務(wù)上,在分布式調(diào)度方面也同樣應(yīng)用非常廣,由于篇幅原因,關(guān)于 Quartz 的詳細(xì)使用介紹,我們會(huì)在后期的文章中詳細(xì)深入分析。
4.2、Spring Schedule
與 Quartz 齊名的還有我們所熟悉的 Spring Schedule,由 Spring 原生提供支持。
實(shí)現(xiàn)上,Spring 中使用定時(shí)任務(wù)也非常簡(jiǎn)單。
4.2.1、基于 XML 配置
- 在springApplication.xml配置文件中加入如下信息
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd">
<!-- 定時(shí)器開(kāi)關(guān)-->
<task:annotation-driven />
<!-- 將TestTask注入ioc-->
<bean id="myTask" class="com.spring.task.TestTask"></bean>
<!-- 定時(shí)調(diào)度方法配置-->
<task:scheduled-tasks>
<!-- 這里表示的是每隔5秒執(zhí)行一次 -->
<task:scheduled ref="myTask" method="show" cron="*/5 * * * * ?" />
<!-- 這里表示的是每隔10秒執(zhí)行一次 -->
<task:scheduled ref="myTask" method="print" cron="*/10 * * * * ?"/>
</task:scheduled-tasks>
<!-- 自動(dòng)掃描的包名 -->
<context:component-scan base-package="com.spring.task" />
</beans>
- 定義自己的任務(wù)執(zhí)行邏輯
package com.spring.task;
/**
* 定義任務(wù)
*/
public class TestTask {
public void show() {
System.out.println("show method 1");
}
public void print() {
System.out.println("print method 1");
}
}
4.2.2、基于注解配置
基于注解的配置,可以直接在方法上配置相應(yīng)的調(diào)度策略,相比xml的方式更加簡(jiǎn)潔。
- 實(shí)現(xiàn)過(guò)程如下
package com.spring.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 基于注解的定時(shí)器
*/
@Component
public class TestTask2 {
/**
* 定時(shí)計(jì)算。每隔5秒執(zhí)行一次
*/
@Scheduled(cron = "*/5 * * * * ?")
public void show() {
System.out.println("show method 2");
}
/**
* 啟動(dòng)1分鐘后執(zhí)行,之后每隔10秒執(zhí)行一次
*/
@Scheduled(initialDelay = 60*1000, fixedRate = 10*1000)
public void print() {
System.out.println("print method 2");
}
}
4.2.3、Spring Boot 定時(shí)任務(wù)應(yīng)用
如果在 Spring Boot 項(xiàng)目中,使用就更加方便了。
- 首先在程序入口啟動(dòng)類(lèi)添加@EnableScheduling,開(kāi)啟定時(shí)任務(wù)功能
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 編寫(xiě)定時(shí)任務(wù)邏輯
@Component
public class TestTask3 {
private int count=0;
@Scheduled(crnotallow="*/5 * * * * ?")
private void process() {
System.out.println("this is scheduler task running "+(count++));
}
}
輸出結(jié)果:
this is scheduler task running 0
this is scheduler task running 1
this is scheduler task running 2
...
4.2.4、任務(wù)執(zhí)行規(guī)則
最后,我們來(lái)看看 Spring Schedule 的任務(wù)執(zhí)行規(guī)則,打開(kāi)@Scheduled注解類(lèi),源碼如下:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String cron() default "";
String zone() default "";
long fixedDelay() default -1;
String fixedDelayString() default "";
long fixedRate() default -1;
String fixedRateString() default "";
long initialDelay() default -1;
String initialDelayString() default "";
}
從方法上可以看出,@Scheduled注解中可以傳 8 種參數(shù):
- cron:指定 cron 表達(dá)式
- zone:默認(rèn)使用服務(wù)器默認(rèn)時(shí)區(qū)??梢栽O(shè)置為java.util.TimeZone中的zoneId
- fixedDelay:從上一個(gè)任務(wù)完成開(kāi)始到下一個(gè)任務(wù)開(kāi)始的間隔,單位毫秒
- fixedDelayString:同上,時(shí)間值是String類(lèi)型
- fixedRate:從上一個(gè)任務(wù)開(kāi)始到下一個(gè)任務(wù)開(kāi)始的間隔,單位毫秒
- fixedRateString:同上,時(shí)間值是String類(lèi)型
- initialDelay:任務(wù)首次執(zhí)行延遲的時(shí)間,單位毫秒
- initialDelayString:同上,時(shí)間值是String類(lèi)型
其中用的最多的就是cron表達(dá)式,下面我們就一起來(lái)看看如何來(lái)編寫(xiě)配置。
4.2.5、Cron 表達(dá)式
Spring 的 cron 表達(dá)式和 Quartz 的 cron 表達(dá)式基本都是通用的,但是與 linux 下 crontab 的 cron 表達(dá)式是有一定區(qū)別的,它可以直接到秒級(jí)別。
cron 表達(dá)式結(jié)構(gòu):
.---------------------- seconds(0 - 59)
| .------------------- minute (0 - 59)
| | .---------------- hour (0 - 23)
| | | .------------- day of month (1 - 31)
| | | | .---------- month (1 - 12)
| | | | | .------- Day-of-Week (1 - 7)
| | | | | | .---- year (1970 - 2099) ...
| | | | | | |
* * * * * ? *
具體樣例如下:
圖片
在 cron 表達(dá)式中不區(qū)分大小寫(xiě),更多配置可以參考這里。
還可以在線解析cron表達(dá)式進(jìn)行測(cè)試。
圖片
4.3、elastic-job
elastic-job 是當(dāng)當(dāng)基于 quartz 二次開(kāi)發(fā)而開(kāi)源的一個(gè)分布式彈性作業(yè)框架,功能十分強(qiáng)大。
主要功能:
- 分布式:重寫(xiě) Quartz 基于數(shù)據(jù)庫(kù)的分布式功能,改用 Zookeeper 實(shí)現(xiàn)注冊(cè)中心。
- 并行調(diào)度:采用任務(wù)分片方式實(shí)現(xiàn)。將一個(gè)任務(wù)拆分為n個(gè)獨(dú)立的任務(wù)項(xiàng),由分布式的服務(wù)器并行執(zhí)行各自分配到的分片項(xiàng)。
- 彈性擴(kuò)容縮容:將任務(wù)拆分為 n 個(gè)任務(wù)項(xiàng)后,各個(gè)服務(wù)器分別執(zhí)行各自分配到的任務(wù)項(xiàng)。一旦有新的服務(wù)器加入集群,或現(xiàn)有服務(wù)器下線,elastic-job將在保留本次任務(wù)執(zhí)行不變的情況下,下次任務(wù)開(kāi)始前觸發(fā)任務(wù)重分片。
- 集中管理:采用基于Zookeeper的注冊(cè)中心,集中管理和協(xié)調(diào)分布式作業(yè)的狀態(tài),分配和監(jiān)聽(tīng)。外部系統(tǒng)可直接根據(jù)Zookeeper的數(shù)據(jù)管理和監(jiān)控elastic-job。
- 定制化流程型任務(wù):作業(yè)可分為簡(jiǎn)單和數(shù)據(jù)流處理兩種模式,數(shù)據(jù)流又分為高吞吐處理模式和順序性處理模式,其中高吞吐處理模式可以開(kāi)啟足夠多的線程快速的處理數(shù)據(jù),而順序性處理模式將每個(gè)分片項(xiàng)分配到一個(gè)獨(dú)立線程,用于保證同一分片的順序性,這點(diǎn)類(lèi)似于kafka的分區(qū)順序性。
由于 elastic-job 是基于 Zookeeper 實(shí)現(xiàn)集群調(diào)度,因此在使用它之前,需要先搭建好 Zookeeper 服務(wù)器,網(wǎng)上教程很多,在此不過(guò)多介紹。
elastic-job 具體簡(jiǎn)單實(shí)現(xiàn)過(guò)程如下!
- 引入相關(guān) elastic-job 依賴(lài)
<dependencies>
<!-- 引入elastic-job-lite核心模塊 -->
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
<version>2.0.5</version>
</dependency>
<!-- 使用springframework自定義命名空間時(shí)引入 -->
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-spring</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
</dependencies>
- 定義job
public class MyElasticJob implements SimpleJob {
public void execute(ShardingContext context) {
System.out.println(context.toString());
switch (context.getShardingItem()) {
case 0:
System.out.println("------------->>>>0");
break;
case 1:
System.out.println("------------->>>>1");
break;
case 2:
System.out.println("------------->>>>2");
break;
default:
System.out.println("------------->>>>default");
break;
}
}
public static void main(String[] args) {
new JobScheduler(createRegistryCenter(), createJobConfiguration()).init();
}
private static CoordinatorRegistryCenter createRegistryCenter() {
//配置ZK注冊(cè)中心地址
CoordinatorRegistryCenter regCenter = new ZookeeperRegistryCenter(new ZookeeperConfiguration("10.211.108.160:2181", "elastic-job-demo"));
regCenter.init();
return regCenter;
}
private static LiteJobConfiguration createJobConfiguration() {
JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder("demoSimpleJob", "0/15 * * * * ?", 10).build();
// 定義SIMPLE類(lèi)型配置
SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, MyElasticJob.class.getCanonicalName());
// 定義Lite作業(yè)根配置
LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).build();
return simpleJobRootConfig;
}
}
具體詳細(xì)使用,可以參考官方網(wǎng)站。
4.4、xxl-job
xxl-job 是被廣泛使用的另外一款使用的分布式任務(wù)調(diào)度框架,出自大眾點(diǎn)評(píng)許雪里(xxl 就是作者名字的拼音首字母)的開(kāi)源項(xiàng)目,官網(wǎng)上介紹這是一個(gè)輕量級(jí)分布式任務(wù)調(diào)度框架,其核心設(shè)計(jì)目標(biāo)是開(kāi)發(fā)迅速、學(xué)習(xí)簡(jiǎn)單、輕量級(jí)、易擴(kuò)展。
圖片
跟elasticjob不同,xxl-job 環(huán)境依賴(lài)于mysql,不用 ZooKeeper,這也是最大的不同。早起的 xxljob 也是基于 quartz 開(kāi)發(fā)的,不過(guò)現(xiàn)在慢慢去 quartz 化了,改成自研的調(diào)度模塊。
xxl-job 具體簡(jiǎn)單實(shí)現(xiàn)過(guò)程如下!
- 引入相關(guān) xxl-job 依賴(lài)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
- 在application.properties添加相關(guān)配置
# web port
server.port=8081
# log config
logging.cnotallow=classpath:logback.xml
spring.application.name=xxljob-demo
### 調(diào)度中心部署跟地址 [選填]:如調(diào)度中心集群部署存在多個(gè)地址則用逗號(hào)分隔。執(zhí)行器將會(huì)使用該地址進(jìn)行"執(zhí)行器心跳注冊(cè)"和"任務(wù)結(jié)果回調(diào)";為空則關(guān)閉自動(dòng)注冊(cè);
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 執(zhí)行器通訊TOKEN [選填]:非空時(shí)啟用;
xxl.job.accessToken=
### 執(zhí)行器AppName [選填]:執(zhí)行器心跳注冊(cè)分組依據(jù);為空則關(guān)閉自動(dòng)注冊(cè)
xxl.job.executor.appname=xxl-job-demo
### 執(zhí)行器注冊(cè) [選填]:優(yōu)先使用該配置作為注冊(cè)地址,為空時(shí)使用內(nèi)嵌服務(wù) ”IP:PORT“ 作為注冊(cè)地址。從而更靈活的支持容器類(lèi)型執(zhí)行器動(dòng)態(tài)IP和動(dòng)態(tài)映射端口問(wèn)題。
xxl.job.executor.address=
### 執(zhí)行器IP [選填]:默認(rèn)為空表示自動(dòng)獲取IP,多網(wǎng)卡時(shí)可手動(dòng)設(shè)置指定IP,該IP不會(huì)綁定Host僅作為通訊實(shí)用;地址信息用于 "執(zhí)行器注冊(cè)" 和 "調(diào)度中心請(qǐng)求并觸發(fā)任務(wù)";
xxl.job.executor.ip=
### 執(zhí)行器端口號(hào) [選填]:小于等于0則自動(dòng)獲??;默認(rèn)端口為9999,單機(jī)部署多個(gè)執(zhí)行器時(shí),注意要配置不同執(zhí)行器端口;
xxl.job.executor.port=9999
### 執(zhí)行器運(yùn)行日志文件存儲(chǔ)磁盤(pán)路徑 [選填] :需要對(duì)該路徑擁有讀寫(xiě)權(quán)限;為空則使用默認(rèn)路徑;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 執(zhí)行器日志文件保存天數(shù) [選填] : 過(guò)期日志自動(dòng)清理, 限制值大于等于3時(shí)生效; 否則, 如-1, 關(guān)閉自動(dòng)清理功能;
xxl.job.executor.logretentinotallow=10
- 編寫(xiě)一個(gè)配置類(lèi)XxlJobConfig
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
- 編寫(xiě)具體任務(wù)類(lèi)XxlJobDemoHandler
@Component
public class XxlJobDemoHandler {
/**
* Bean模式,一個(gè)方法為一個(gè)任務(wù)
* 1、在Spring Bean實(shí)例中,開(kāi)發(fā)Job方法,方式格式要求為 "public ReturnT<String> execute(String param)"
* 2、為Job方法添加注解 "@XxlJob(value="自定義jobhandler名稱(chēng)", init = "JobHandler初始化方法", destroy = "JobHandler銷(xiāo)毀方法")",注解value值對(duì)應(yīng)的是調(diào)度中心新建任務(wù)的JobHandler屬性的值。
* 3、執(zhí)行日志:需要通過(guò) "XxlJobLogger.log" 打印執(zhí)行日志;
*/
@XxlJob("demoJobHandler")
public ReturnT<String> demoJobHandler(String param) throws Exception {
XxlJobLogger.log("java, Hello World~~~");
XxlJobLogger.log("param:" + param);
return ReturnT.SUCCESS;
}
}
寫(xiě)完之后啟動(dòng)服務(wù),然后可以打開(kāi)管理界面,找到執(zhí)行器管理,添加執(zhí)行器。
圖片
接著到任務(wù)管理,添加任務(wù)。
最后我們可以到任務(wù)管理去測(cè)試一下,運(yùn)行demoJobHandler。
圖片
圖片
點(diǎn)擊保存后,會(huì)立即執(zhí)行。點(diǎn)擊查看日志,可以看到任務(wù)執(zhí)行的歷史日志記錄
圖片
這就是簡(jiǎn)單的Demo演示,具體詳細(xì)使用,可以參考官方網(wǎng)站。
五、總結(jié)
本文主要圍繞定時(shí)調(diào)度器的理論知識(shí)和用法做了一次知識(shí)的總結(jié),如果有理解不對(duì)的地方,歡迎大家留言指出。
六、參考
1、https://juejin.im/post/6844904002606350343
2、https://developer.ibm.com/zh/languages/java/articles/j-lo-taskschedule/
3、https://www.cnblogs.com/linjiqin/p/11720673.html
4、https://www.cnkirito.moe/timer/
5、https://cloud.tencent.com/developer/article/1138669
6、https://developer.aliyun.com/article/775305