定時任務總結,全系列…
前言
在不用的業務場景下要用不同的定時任務,其實我們的選擇還是挺多的。本文總結了 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 | 指定用戶 |
-e | 編輯某個用戶的crontab文件內容 |
-l | 顯示某個用戶的crontab文件內容 |
-r | 刪除某用戶的crontab文件 |
-i | 刪除某用戶的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 框架自帶的定時功能,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
用於存儲作業和任務調度期間的狀態。
項目實戰
我們還是以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: //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/Saturn/。
2. TBSchedule
TBSchedule 是阿里開發的一款分散式任務調度平台,旨在將調度作業從業務系統中分離出來,降低或者是消除和業務系統的耦合度,進行高效非同步任務處理。
目前被廣泛應用在阿里巴巴、淘寶、支付寶、京東、聚美、汽車之家、國美等很多互聯網企業的流程調度系統中。
github 地址://github.com/taobao/TBSchedule。