前言
最近有幾個讀者私信給我,問我他們的業務場景,要用什麼樣的定時任務。確實,在不用的業務場景下要用不同的定時任務,其實我們的選擇還是挺多的。我今天給大家總結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 | |
-i |
以上引數,如果沒有使用 -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
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
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
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@SpringBootApplicationpublic class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args); }}
第三步,使用 @Scheduled 註解定義定時規則。
@Servicepublic 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-12,支援 * 、 , 、 - 、 / 。* , - / ? ? 日期 星期常見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點執行
常見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 用於儲存作業和任務排程期間的狀態。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 。
@Configurationpublic 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
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-samplexxl.job.executor.port: 8888xxl.job.executor.logpath: /data/applogs/xxl-job/
第三步,建立HelloJobHandler類繼承 IJobHandler 類:
@JobHandler(value = "helloJobHandler")@Componentpublic 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 專案下的一個子專案,官網地址:http://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配置任務:
@Configurationpublic 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=elasticjobDemozk.serverList=localhost:2181zk.namespace=elasticjobDemosue.spring.elatisc.cron=0/5 * * * * ?sue.spring.elatisc.itemParameters=0=A,1=B,2=C,3=Dsue.spring.elatisc.jobParameters=testsue.spring.elatisc.shardingTotalCount=4
這樣定時任務就配置好了,建立定時任務的步驟,相對於 xxl-job 來說要繁瑣一些。
使用 elastic-job 的優缺點:優點:支援分散式排程協調,支援分片,適合高併發,和一些業務相對來說較複雜的場景。缺點:需要依賴於zookeeper,實現定時任務相對於 xxl-job 要複雜一些,要對分片規則非常熟悉。3.其他分散式定時任務1. Saturn
1. Saturn
Saturn是唯品會開源的一個分散式任務排程平臺。取代傳統的Linux Cron/Spring Batch Job的方式,做到全域統一配置,統一監控,任務高可用以及分片併發處理。
Saturn是在噹噹開源的Elastic-Job基礎上,結合各方需求和我們的實踐見解改良而成。使用案例:唯品會、酷狗音樂、新網銀行、海融易、航美線上、量富徵信等。
github地址:https://github.com/vipshop/Saturn/
2. TBSchedule
TBSchedule是阿里開發的一款分散式任務排程平臺,旨在將排程作業從業務系統中分離出來,降低或者是消除和業務系統的耦合度,進行高效非同步任務處理。
目前被廣泛應用在阿里巴巴、淘寶、支付寶、京東、聚美、汽車之家、國美等很多網際網路企業的流程排程系統中。
github地址:https://github.com/taobao/TBSchedule
老實說優秀的定時任務還是挺多的,不是說哪種定時任務牛逼我們就 一定要 不是哪種,而是要根據實際業務需求選擇。每種定時任務都有優缺點,合理選擇既能滿足業務需求,又能避免資源浪費,才是上上策。當然在實際的業務場景,通常會有多種定時任務一起 配合 使用。
原文連結:https://mp.weixin.qq.com/s?__biz=MzUxODkzNTQ3Nw==&mid=2247486724&idx=1&sn=541e79fe6eeb5d73615d9cfff34447ce
-
1 #學會這10種定時任務,我有點飄了
-
2 #學會這10種定時任務,我有點飄了
-
3 #學會這10種定時任務,我有點飄了
-
4 #學會這10種定時任務,我有點飄了
-
5 #學會這10種定時任務,我有點飄了
-
6 #學會這10種定時任務,我有點飄了
-
7 #學會這10種定時任務,我有點飄了
-
8 #學會這10種定時任務,我有點飄了
-
9 #學會這10種定時任務,我有點飄了
-
10 #學會這10種定時任務,我有點飄了
-
11 #學會這10種定時任務,我有點飄了
-
12 #學會這10種定時任務,我有點飄了
-
13 #學會這10種定時任務,我有點飄了
-
14 #學會這10種定時任務,我有點飄了
-
15 #學會這10種定時任務,我有點飄了