前言
最近有幾個讀者私信給我,問我他們的業務場景,要用什麼樣的定時任務。確實,在不用的業務場景下要用不同的定時任務,其實我們的選擇還是挺多的。我今天給大家總結10種非常實用的定時任務,總有一種是適合你的。
一. linux自帶的定時任務
crontab
不知道你有沒有遇到過這種場景:有時需要臨時統計線上的數據,然後導出到excel表格中。這種需求有時較爲複雜,光靠寫sql語句是無法滿足需求的,這就需要寫java代碼了。然後將該程序打成一個jar包,在線上環境執行,最後將生成的excel文件下載到本地。
爲了減小對線上環境的影響,我們一般會選擇在凌晨1-2點
,趁用戶量少的時候,執行統計程序。(其實凌晨4點左右,用戶纔是最少的)
由於時間太晚了,我們完全沒必要守在那裏等執行結果,一個定時任務就能可以搞定。
那麼,這種情況用哪種定時任務更合適呢?
答案是:linux
系統的crontab
。(不過也不排除有些項目沒部署在linux系統中)
運行crontab -e
,可以編輯定時器,然後加入如下命令:
0 2 * * * /usr/local/java/jdk1.8/bin/java -jar /data/app/tool.jar > /logs/tool.log &
就可以在每天凌晨2點
,定時執行tool.jar
程序,並且把日誌輸出到tool.log
文件中。當然你也可以把後面的執行java程序的命令寫成shell腳本,更方便維護。
使用這種定時任務支持方便修改定時規則,有界面可以統一管理配置的各種定時腳本。
crontab命令的基本格式如下:
crontab [參數] [文件名]
如果沒有指定文件名,則接收鍵盤上輸入的命令,並將它載入到crontab
。
參數功能對照表如下:
以上參數,如果沒有使用-u
指定用戶,則默認使用的當前用戶。
通過crontab -e
命令編輯文件內容,具體語法如下:
[分] [小時] [日期] [月] [星期] 具體任務
其中:
分,表示多少分鐘,範圍:0-59
小時,表示多少小時,範圍:0-23
日期,表示具體在哪一天,範圍:1-31
月,表示多少月,範圍:1-12
星期,表示多少周,範圍:0-7,0和7都代表星期日
還有一些特殊字符,比如:
*
代表如何時間,比如:*1***
表示每天凌晨1點執行。/
代表每隔多久執行一次,比如:*/5 ****
表示每隔5分鐘執行一次。,
代表支持多個,比如:10 7,9,12 ***
表示在每天的7、9、12點10分各執行一次。-
代表支持一個範圍,比如:10 7-9 ***
表示在每天的7、8、9點10分各執行一次。
此外,順便說一下crontab
需要crond
服務支持,crond
是linux
下用來週期地執行某種任務的一個守護進程,在安裝linux
操作系統後,默認會安裝crond
服務工具,且crond
服務默認就是自啓動的。crond
進程每分鐘會定期檢查是否有要執行的任務,如果有,則會自動執行該任務。
可以通過以下命令操作相關服務:
service crond status // 查看運行狀態
service crond start //啓動服務
service crond stop //關閉服務
service crond restart //重啓服務
service crond reload //重新載入配置
使用crontab
的優缺點:
優點:方便修改定時規則,支持一些較複雜的定時規則,通過文件可以統一管理配好的各種定時腳本。
缺點:如果定時任務非常多,不太好找,而且必須要求操作系統是
linux
,否則無法執行。
二. jdk自帶的定時任務
1.Thread
各位親愛的朋友,你沒看錯,Thread
類真的能做定時任務。如果你看過一些定時任務框架的源碼,你最後會發現,它們的底層也會使用Thread
類。
實現這種定時任務的具體代碼如下:
public static void init() {
new Thread(() -> {
while (true) {
try {
System.out.println("doSameThing");
Thread.sleep(1000 * 60 * 5);
} catch (Exception e) {
log.error(e);
}
}
}).start();
}
使用Thread
類可以做最簡單的定時任務,在run
方法中有個while
的死循環(當然還有其他方式),執行我們自己的任務。有個需要特別注意的地方是,需要用try...catch
捕獲異常,否則如果出現異常,就直接退出循環,下次將無法繼續執行了。
這種方式做的定時任務,只能週期性執行,不能支持定時在某個時間點執行。
此外,該線程可以定義成守護線程
,在後臺默默執行就好。
使用場景:比如項目中有時需要每隔10分鐘去下載某個文件,或者每隔5分鐘去讀取模板文件生成靜態html頁面等等,一些簡單的週期性任務場景。
使用Thread
類的優缺點:
優點:這種定時任務非常簡單,學習成本低,容易入手,對於那些簡單的週期性任務,是個不錯的選擇。
缺點:不支持指定某個時間點執行任務,不支持延遲執行等操作,功能過於單一,無法應對一些較爲複雜的場景。
2.Timer
Timer
類是jdk專門提供的定時器工具,用來在後臺線程計劃執行指定任務,在java.util
包下,要跟TimerTask
一起配合使用。
Timer
類其實是一個任務調度器,它裏面包含了一個TimerThread
線程,在這個線程中無限循環從TaskQueue
中獲取TimerTask
(該類實現了Runnable接口),調用其run
方法,就能異步執行定時任務。我們需要繼承TimerTask
類,實現它的run
方法,在該方法中加上自己的業務邏輯。
實現這種定時任務的具體代碼如下:
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("doSomething");
}
},2000,1000);
}
}
先實例化一個Timer
類,然後調用它的schedule
方法,在該方法中實例化TimerTask
類,業務邏輯寫在run
方法中。schedule
方法最後的兩次參數分別表示:延遲時間
和 間隔時間
,單位是毫秒。上面例子中,設置的定時任務是每隔1秒執行一次,延遲2秒執行。
主要包含6個方法:
schedule(TimerTask task, Date time)
, 指定任務task在指定時間time執行schedule(TimerTask task, long delay)
, 指定任務task在指定延遲delay後執行schedule(TimerTask task, Date firstTime,long period)
,指定任務task在指定時間firstTime執行後,進行重複固定延遲頻率peroid的執行schedule(TimerTask task, long delay, long period)
, 指定任務task 在指定延遲delay 後,進行重複固定延遲頻率peroid的執行scheduleAtFixedRate(TimerTask task,Date firstTime,long period)
, 指定任務task在指定時間firstTime執行後,進行重複固定延遲頻率peroid的執行scheduleAtFixedRate(TimerTask task, long delay, long period)
, 指定任務task 在指定延遲delay 後,進行重複固定延遲頻率peroid的執行
不過使用Timer
實現定時任務有以下問題:
由於
Timer
是單線程執行任務,如果其中一個任務耗時非常長,會影響其他任務的執行。如果
TimerTask
拋出RuntimeException
,Timer會停止所有任務的運行。
使用Timer
類的優缺點:
優點:非常方便實現多個週期性的定時任務,並且支持延遲執行,還支持在指定時間之後支持,功能還算強大。
缺點:如果其中一個任務耗時非常長,會影響其他任務的執行。並且如果
TimerTask
拋出RuntimeException
,Timer
會停止所有任務的運行,所以阿里巴巴開發者規範中不建議使用它。
3.ScheduledExecutorService
ScheduledExecutorService
是JDK1.5+版本引進的定時任務,該類位於java.util.concurrent
併發包下。
ScheduledExecutorService
是基於多線程的,設計的初衷是爲了解決Timer
單線程執行,多個任務之間會互相影響的問題。
它主要包含4個方法:
schedule(Runnable command,long delay,TimeUnit unit)
,帶延遲時間的調度,只執行一次,調度之後可通過Future.get()阻塞直至任務執行完畢。schedule(Callable<V> callable,long delay,TimeUnit unit)
,帶延遲時間的調度,只執行一次,調度之後可通過Future.get()阻塞直至任務執行完畢,並且可以獲取執行結果。scheduleAtFixedRate
,表示以固定頻率執行的任務,如果當前任務耗時較多,超過定時週期period,則當前任務結束後會立即執行。scheduleWithFixedDelay
,表示以固定延時執行任務,延時是相對當前任務結束爲起點計算開始時間。
實現這種定時任務的具體代碼如下:
public class ScheduleExecutorTest {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("doSomething");
},1000,1000, TimeUnit.MILLISECONDS);
}
}
調用ScheduledExecutorService
類的scheduleAtFixedRate
方法實現週期性任務,每隔1秒鐘執行一次,每次延遲1秒再執行。
這種定時任務是阿里巴巴開發者規範中用來替代Timer
類的方案,對於多線程執行週期性任務,是個不錯的選擇。
ScheduledExecutorService的優缺點:
優點:基於多線程的定時任務,多個任務之間不會相關影響,支持週期性的執行任務,並且帶延遲功能。
缺點:不支持一些較複雜的定時規則。
三. spring支持的定時任務
1.spring task
spring task
是spring3
以上版本自帶的定時任務,實現定時任務的功能時,需要引入spring-context
包,目前它支持:xml
和 註解
兩種方式。
1. 項目實戰
由於xml方式太古老了,我們以springboot項目中註解方式爲例。
第一步,在pom.xml文件中引入spring-context
相關依賴。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
第二步,在springboot啓動類上加上@EnableScheduling
註解。
@EnableScheduling
@SpringBootApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
}
}
第三步,使用@Scheduled
註解定義定時規則。
@Service
public class SpringTaskTest {
@Scheduled(cron = "${sue.spring.task.cron}")
public void fun() {
System.out.println("doSomething");
}
}
第四步,在applicationContext.properties
文件中配置參數:
sue.spring.task.cron=*/10 * * * * ?
這樣就能每隔10秒執行一次fun方法了。
2. cron規則
spring4以上的版本中,cron表達式包含6個參數:
[秒] [分] [時] [日期] [月] [星期]
還支持幾個常用的特殊符號:
*
:表示任何時間觸發任務,
:表示指定的時間觸發任務-
:表示一段時間內觸發任務/
:表示從哪一個時刻開始,每隔多長時間觸發一次任務。?
:表示用於月中的天和週中的天兩個子表達式,表示不指定值。
cron表達式參數具體含義:
秒,取值範圍:0-59,支持
*
、,
、-
、/
。分,取值範圍:0-59,支持
*
、,
、-
、/
。時,取值範圍:0-23,支持
*
、,
、-
、/
。日期,取值範圍:1-31,支持
*
、,
、-
、/
。比秒多了?
,表示如果指定的星期
觸發了,則配置的日期
變成無效。月,取值範圍:1-12,支持
*
、,
、-
、/
。星期,取值範圍:1~7,1代表星期天,6代表星期六,其他的以此類推。支持
*
、,
、-
、/
、?
。比秒多了?
,表示如果指定的日期
觸發了,則配置的星期
變成無效。
常見cron表達式使用舉例:
0 0 0 1 * ?
每月1號零點執行0 0 2 * * ?
每天凌晨2點執行0 0 2 * * ?
每天凌晨2點執行0 0/5 11 * * ?
每天11點-11點55分,每隔5分鐘執行一次0 0 18 ? * WED
每週三下午6點執行
spring task先通過ScheduledAnnotationBeanPostProcessor類的processScheduled方法,解析和收集Scheduled
註解中的參數,包含:cron表達式。
然後在ScheduledTaskRegistrar類的afterPropertiesSet方法中,默認初始化一個單線程的ThreadPoolExecutor
執行任務。
對spring task感興趣的小夥伴,可以加我微信找我私聊。
使用spring task
的優缺點:
優點:spring框架自帶的定時功能,springboot做了非常好的封裝,開啓和定義定時任務非常容易,支持複雜的
cron
表達式,可以滿足絕大多數單機版的業務場景。單個任務時,當前次的調度完成後,再執行下一次任務調度。缺點:默認單線程,如果前面的任務執行時間太長,對後面任務的執行有影響。不支持集羣方式部署,不能做數據存儲型定時任務。
2.spring quartz
quartz
是OpenSymphony
開源組織在Job scheduling
領域的開源項目,是由java開發的一個開源的任務日程管理系統。
quartz能做什麼?
作業調度:調用各種框架的作業腳本,例如shell,hive等。
定時任務:在某一預定的時刻,執行你想要執行的任務。
架構圖如下:
quartz包含的主要接口如下:
Scheduler
代表調度容器,一個調度容器中可以註冊多個JobDetail和Trigger。Job
代表工作,即要執行的具體內容。JobDetail
代表具體的可執行的調度程序,Job是這個可執行程調度程序所要執行的內容。JobBuilder
用於定義或構建JobDetail實例。Trigger
代表調度觸發器,決定什麼時候去調。TriggerBuilder
用於定義或構建觸發器。JobStore
用於存儲作業和任務調度期間的狀態。
1. 項目實戰
我們還是以springboot
集成quartz
爲例。
第一步,在pom.xml文件中引入quartz
相關依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
第二步,創建真正的定時任務執行類,該類繼承QuartzJobBean
。
public class QuartzTestJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
String userName = (String) context.getJobDetail().getJobDataMap().get("userName");
System.out.println("userName:" + userName);
}
}
第三步,創建調度程序JobDetail
和調度器Trigger
。
@Configuration
public class QuartzConfig {
@Value("${sue.spring.quartz.cron}")
private String testCron;
/**
* 創建定時任務
*/
@Bean
public JobDetail quartzTestDetail() {
JobDetail jobDetail = JobBuilder.newJob(QuartzTestJob.class)
.withIdentity("quartzTestDetail", "QUARTZ_TEST")
.usingJobData("userName", "susan")
.storeDurably()
.build();
return jobDetail;
}
/**
* 創建觸發器
*/
@Bean
public Trigger quartzTestJobTrigger() {
//每隔5秒執行一次
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(testCron);
//創建觸發器
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(quartzTestDetail())
.withIdentity("quartzTestJobTrigger", "QUARTZ_TEST_JOB_TRIGGER")
.withSchedule(cronScheduleBuilder)
.build();
return trigger;
}
}
第四步,在applicationContext.properties
文件中配置參數:
sue.spring.quartz.cron=*/5 * * * * ?
這樣就能每隔5秒執行一次QuartzTestJob類的executeInternal方法了。
CronTrigger配置格式:
[秒] [分] [小時] [日] [月] [周] [年]
spring quartz
跟spring task
的cron
表達式規則基本一致,只是spring4
以上的版本去掉了後面的年
,而quartz
的CronTrigger
的年
是非必填的,這裏我就不做過多介紹了。
使用spring quartz
的優缺點:
優點:默認是多線程異步執行,單個任務時,在上一個調度未完成時,下一個調度時間到時,會另起一個線程開始新的調度,多個任務之間互不影響。支持複雜的
cron
表達式,它能被集羣實例化,支持分佈式部署。缺點:相對於spring task實現定時任務成本更高,需要手動配置
QuartzJobBean
、JobDetail
和Trigger
等。需要引入了第三方的quartz
包,有一定的學習成本。不支持並行調度,不支持失敗處理策略和動態分片的策略等。
四. 分佈式定時任務
1.xxl-job
xxl-job
是大衆點評(許雪裏)開發的一個分佈式任務調度平臺,其核心設計目標是開發迅速、學習簡單、輕量級、易擴展。現已開放源代碼並接入多家公司線上產品線,開箱即用。
xxl-job
框架對quartz
進行了擴展,使用mysql
數據庫存儲數據,並且內置jetty作爲RPC
服務調用。
主要特點如下:
有界面維護定時任務和觸發規則,非常容易管理。
能動態啓動或停止任務
支持彈性擴容縮容
支持任務失敗報警
支持動態分片
支持故障轉移
Rolling實時日誌
支持用戶和權限管理
管理界面:
使用quartz架構圖如下:
項目實戰
xxl-admin
管理後臺部署和mysql腳本執行等這些前期準備工作,我就不過多介紹了,有需求的朋友可以找我私聊,這些更偏向於運維的事情。
假設前期工作已經OK了,接下來我們需要:
第一步,在pom.xml文件中引入xxl-job
相關依賴。
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
第二步,在applicationContext.properties
文件中配置參數:
xxl.job.admin.address: http://localhost:8088/xxl-job-admin/
xxl.job.executor.appname: xxl-job-executor-sample
xxl.job.executor.port: 8888
xxl.job.executor.logpath: /data/applogs/xxl-job/
第三步,創建HelloJobHandler類繼承IJobHandler
類:
@JobHandler(value = "helloJobHandler")
@Component
public class HelloJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) {
System.out.println("XXL-JOB, Hello World.");
return SUCCESS;
}
}
這樣定時任務就配置好了。
建議把定時任務單獨部署到另外一個服務中,跟api服務分開。根據我以往的經驗,job大部分情況下,會對數據做批量操作,如果操作的數據量太大,可能會對服務的內存和cpu資源造成一定的影響。
使用xxl-job
的優缺點:
優點:有界面管理定時任務,支持彈性擴容縮容、動態分片、故障轉移、失敗報警等功能。它的功能非常強大,很多大廠在用,可以滿足絕大多數業務場景。
缺點:和
quartz
一樣,通過數據庫分佈式鎖,來控制任務不能重複執行。在任務非常多的情況下,有一些性能問題。
2.elastic-job
elastic-job
是噹噹網開發的彈性分佈式任務調度系統,功能豐富強大,採用zookeeper實現分佈式協調,實現任務高可用以及分片。它是專門爲高併發和複雜業務場景開發。
elastic-job
目前是apache
的shardingsphere
項目下的一個子項目,官網地址:shardingsphere.apache.org/elasticjob/…
elastic-job
在2.x之後,出了兩個產品線:Elastic-Job-Lite
和Elastic-Job-Cloud
,而我們一般使用Elastic-Job-Lite就能夠滿足需求。Elastic-Job-Lite定位爲輕量級無中心化解決方案,使用jar包的形式提供分佈式任務的協調服務,外部僅依賴於Zookeeper。。
主要特點如下:
分佈式調度協調
彈性擴容縮容
失效轉移
錯過執行作業重觸發
作業分片一致性,保證同一分片在分佈式環境中僅一個執行實例
自診斷並修復分佈式不穩定造成的問題
支持並行調度
整體架構圖:
項目實戰
第一步,在pom.xml文件中引入elastic-job
相關依賴。
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-spring</artifactId>
</dependency>
第二步,增加ZKConfig類,配置zookeeper
:
@Configuration
@ConditionalOnExpression("'${zk.serverList}'.length() > 0")
public class ZKConfig {
@Bean
public ZookeeperRegistryCenter registry(@Value("${zk.serverList}") String serverList,
@Value("${zk.namespace}") String namespace) {
return new ZookeeperRegistryCenter(new ZookeeperConfiguration(serverList, namespace));
}
}
第三步,定義一個類實現SimpleJob
接口:
public class TestJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext){
System.out.println("ShardingTotalCount:"+shardingContext.getShardingTotalCount());
System.out.println("ShardingItem:"+shardingContext.getShardingItem());
}
}
第四步,增加JobConfig配置任務:
@Configuration
public class JobConfig {
@Value("${sue.spring.elatisc.cron}")
private String testCron;
@Value("${sue.spring.elatisc.itemParameters}")
private String shardingItemParameters;
@Value("${sue.spring.elatisc.jobParameters}")
private String jobParameters =;
@Value("${sue.spring.elatisc.shardingTotalCount}")
private int shardingTotalCount;
@Autowired
private ZookeeperRegistryCenter registryCenter;
@Bean
public SimpleJob testJob() {
return new TestJob();
}
@Bean
public JobScheduler simpleJobScheduler(final SimpleJob simpleJob) {
return new SpringJobScheduler(simpleJob, registryCenter, getConfiguration(simpleJob.getClass(),
cron, shardingTotalCount, shardingItemParameters, jobParameters));
}
private geConfiguration getConfiguration(Class<? extends SimpleJob> jobClass,String cron,int shardingTotalCount,String shardingItemParameters,String jobParameters) {
JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder(jobClass.getName(), testCron, shardingTotalCount).
shardingItemParameters(shardingItemParameters).jobParameter(jobParameters).build();
SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, jobClass.getCanonicalName());
LiteJobConfiguration jobConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).overwrite(true).build();
return jobConfig;
}
}
其中:
cron:cron表達式,定義觸發規則。
shardingTotalCount:定義作業分片總數
shardingItemParameters:定義分配項參數,一般用分片序列號和參數用等號分隔,多個鍵值對用逗號分隔,分片序列號從0開始,不可大於或等於作業分片總數。
jobParameters:作業自定義參數
第五步,在applicationContext.properties
文件中配置參數:
spring.application.name=elasticjobDemo
zk.serverList=localhost:2181
zk.namespace=elasticjobDemo
sue.spring.elatisc.cron=0/5 * * * * ?
sue.spring.elatisc.itemParameters=0=A,1=B,2=C,3=D
sue.spring.elatisc.jobParameters=test
sue.spring.elatisc.shardingTotalCount=4
這樣定時任務就配置好了,創建定時任務的步驟,相對於xxl-job
來說要繁瑣一些。
使用elastic-job
的優缺點:
優點:支持分佈式調度協調,支持分片,適合高併發,和一些業務相對來說較複雜的場景。
缺點:需要依賴於zookeeper,實現定時任務相對於
xxl-job
要複雜一些,要對分片規則非常熟悉。
3.其他分佈式定時任務
1. Saturn
Saturn是唯品會開源的一個分佈式任務調度平臺。取代傳統的Linux Cron/Spring Batch Job的方式,做到全域統一配置,統一監控,任務高可用以及分片併發處理。
Saturn是在噹噹開源的Elastic-Job基礎上,結合各方需求和我們的實踐見解改良而成。使用案例:唯品會、酷狗音樂、新網銀行、海融易、航美在線、量富徵信等。
github地址:github.com/vipshop/Sat…
2. TBSchedule
TBSchedule是阿里開發的一款分佈式任務調度平臺,旨在將調度作業從業務系統中分離出來,降低或者是消除和業務系統的耦合度,進行高效異步任務處理。
目前被廣泛應用在阿里巴巴、淘寶、支付寶、京東、聚美、汽車之家、國美等很多互聯網企業的流程調度系統中。
github地址:github.com/taobao/TBSc…
老實說優秀的定時任務還是挺多的,不是說哪種定時任務牛逼我們就一定要用哪種,而是要根據實際業務需求選擇。每種定時任務都有優缺點,合理選擇既能滿足業務需求,又能避免資源浪費,纔是上上策。當然在實際的業務場景,通常會多種定時任務一起配合使用。
順便說一句,歡迎親愛的小夥伴們,找我一起聊聊:你用過哪些定時任務,遇到過哪些問題,以及如何解決問題的。如果有相關問題也可以問我。