本文主要介紹目前存在的定時任務處理解決方案。業務系統中存在衆多的任務需要定時或定期執行,並且針對不同的系統架構也需要提供不同的解決方案。京東內部也提供了衆多定時任務中間件來支持,總結當前各種定時任務原理,從定時任務基礎原理、單機定時任務(單線程、多線程)、分佈式定時任務介紹目前主流的定時任務的基本原理組成、優缺點等。希望能幫助讀者深入理解定時任務具體的算法和實現方案。
一、背景概述
定時任務,顧名思義,就是指定時間點進行執行相應的任務。業務場景中包括:
-
每天晚上12點,將當日的銷售數據發送給各個VP;
-
訂單下單十分鐘未付款將自動取消訂單;用戶下單後發短信;
-
定時的清理系統中失效的數據;
-
心跳檢測、session、請求是否timeout。
二、定時任務基礎原理
2.1 小頂堆算法
每個節點是對應的定時任務,定時任務的執行順序通過利用堆化進行排序,循環判斷每秒是否堆頂的任務是否應該執行,每次插入任務、刪除任務需要重新堆化;
圖1 利用小頂堆來獲取需要最新執行的任務
爲什麼用優先隊列(小頂堆)而不是有序的數組或者鏈表?
因爲優先隊列只需要確保局部有序,它的插入、刪除操作的複雜度都是O(log n);而有序數組的插入和刪除複雜度爲O(n);鏈表的插入複雜度爲O(n),刪除複雜度爲O(1)。總體而言優先隊列性能最好。
2.2 時間輪算法
鏈表或者數組實現時間輪:
圖2 利用鏈表+數組實現時間輪算法
round時間輪: 時間輪其實就是一種環型的數據結構,可以把它想象成一個時鐘,分成了許多格子,每個格子代表一定的時間,在這個格子上用一個鏈表來保存要執行的超時任務,同時有一個指針一格一格的走,走到那個格子時就執行格子對應的延遲任務。
圖3 環形數據結構的round時間輪
2.3 分層時間輪
就是將月、周、天分成不同的時間輪層級,各自的時間輪進行定義:
圖4 按時間維度分層的時間輪
三、單機定時任務
3.1 單線程任務調度
3.1.1 無限循環
創建thread,在while中一直執行,通過sleep來達到定時任務的效果。
3.1.2 JDK提供了Timer
Timer位於java.util包下,其內部包含且僅包含一個後臺線程(TimeThread)對多個業務任務(TimeTask)進行定時定頻率的調度。
圖5 JDK中Timer支持的調度方法
每個Timer中包含一個TaskQueue對象,這個隊列存儲了所有將被調度的task, 該隊列是一個根據task下一次運行時間排序形成的最小優先隊列,該最小優先隊列的是一個二叉堆,所以可以在log(n)的時間內完成增加task,刪除task等操作,並且可以在常數時間內獲得下次運行時間最小的task對象。
原理: TimerTask是按nextExecutionTime進行堆排序的。每次取堆中nextExecutionTime和當前系統時間進行比較,如果當前時間大於nextExecutionTime則執行,如果是單次任務,會將任務從最小堆,移除。否則,更新nextExecutionTime的值。
圖6 TimerTask中按照時間的堆排序
任務追趕特性:
schedule在執行的時候,如果Date過了,也就是Date是小於現在時間,那麼會立即執行一次,然後從這個執行時間開始每隔間隔時間執行一次;
scheduleAtFixedRate在執行的時候,如果Date過了。還會執行,然後纔是每隔一段時間執行。
Timer問題:
-
任務執行時間長影響其他任務:如果TimerTask拋出未檢查的異常,Timer將會產生無法預料的行爲。Timer線程並不捕獲異常,所以 TimerTask拋出的未檢查的異常會終止timer線程。此時,已經被安排但尚未執行的TimerTask永遠不會再執行了,新的任務也不能被調度了。
-
任務異常影響其他任務:Timer裏面的任務如果執行時間太長,會獨佔Timer對象,使得後面的任務無法幾時的執行 ,ScheduledExecutorService不會出現Timer的問題(除非你只搞一個單線程池的任務區)。
3.1.3 DelayQueue
DelayQueue 是一個支持延時獲取元素的無界阻塞隊列,DelayQueue 其實就是在每次往優先級隊列中添加元素,然後以元素的delay過期值作爲排序的因素,以此來達到先過期的元素會拍在隊首,每次從隊列裏取出來都是最先要過期的元素。
-
delayed是一個具有過期時間的元素
-
PriorityQueue是一個根據隊列裏元素某些屬性排列先後的順序隊列(核心還是基於小頂堆)
隊列中的元素必須實現 Delayed 接口,並重寫 getDelay(TimeUnit) 和 compareTo(Delayed) 方法。
-
CompareTo(Delayed o):Delayed接口繼承了Comparable接口,因此有了這個方法。
-
getDelay(TimeUnit unit):這個方法返回到激活日期的剩餘時間,時間單位由單位參數指定。
隊列入隊出隊方法:
-
offer():入隊的邏輯綜合了PriorityBlockingQueue的平衡二叉堆冒泡插入以及DelayQueue的消費線程喚醒與leader領導權剝奪
-
take():出隊的邏輯一樣綜合了PriorityBlockingQueue的平衡二叉堆向下降級以及DelayQueue的Leader-Follower線程等待喚醒模式
在ScheduledExecutorService中推出了DelayedWorkQueue,DelayQueue隊列元素必須是實現了Delayed接口的實例,而DelayedWorkQueue存放的是線程運行時代碼RunnableScheduledFuture,該延時隊列靈活的加入定時任務特有的方法調用。
圖7 定時任務中的延時隊列類圖
leader follower模式:
所有線程會有三種身份中的一種:leader和follower,以及一個工作中的狀態:proccesser。它的基本原則就是,永遠最多隻有一個leader。而所有follower都在等待成爲leader。線程池啓動時會自動產生一個Leader負責等待網絡IO事件,當有一個事件產生時,Leader線程首先通知一個Follower線程將其提拔爲新的Leader,然後自己就去幹活了,去處理這個網絡事件,處理完畢後加入Follower線程等待隊列,等待下次成爲Leader。這種方法可以增強CPU高速緩存相似性,及消除動態內存分配和線程間的數據交換。
3.1.4 Netty 實現延遲任務-HashedWheel
可以使用 Netty 提供的工具類 HashedWheelTimer 來實現延遲任務。
該工具類採用的是時間輪的原理來實現的,HashedWheelTimer是一個基於hash的環形數組。
圖8 HashedWheelTimer實現的時間輪
1. 優點: 能高效的處理大批定時任務,適用於對時效性不高的,可快速執行的,大量這樣的“小”任務,能夠做到高性能,低消耗。把大批量的調度任務全部都綁定到同一個的調度器上面,使用這一個調度器來進行所有任務的管理(manager),觸發(trigger)以及運行(runnable)。能夠高效的管理各種延時任務,週期任務,通知任務等等。
2. 缺點: 對內存要求較高,佔用較高的內存。時間精度要求不高:時間輪調度器的時間精度可能不是很高,對於精度要求特別高的調度任務可能不太適合。因爲時間輪算法的精度取決於,時間段“指針”單元的最小粒度大小,比如時間輪的格子是一秒跳一次,那麼調度精度小於一秒的任務就無法被時間輪所調度。
3.1.5 MQ 實現延遲任務
-
訂單在十分鐘之內未支付則自動取消。
-
新創建的店鋪,如果在十天內都沒有上傳過商品,則自動發送消息提醒。
-
賬單在一週內未支付,則自動結算。
-
用戶註冊成功後,如果三天內沒有登陸則進行短信提醒。
-
用戶發起退款,如果三天內沒有得到處理則通知相關運營人員。
-
預定會議後,需要在預定的時間點前十分鐘通知各個與會人員參加會議。
以上這些場景都有一個特點,需要在某個事件發生之後或者之前的指定時間點完成某一項任務。
RabbitMQ 實現延遲隊列的方式有兩種:
- 通過消息過期後進入死信交換器,再由交換器轉發到延遲消費隊列,實現延遲功能;
<!---->
- 使用 rabbitmq-delayed-message-exchange 插件實現延遲功能。
同樣我們也可以利用京東自研jmq的延時消費來做到以上的場景。
3.2 多線程定時任務
上述方案都是基於單線程的任務調度,如何引入多線程提高延時任務的併發處理能力?
3.2.1 ScheduledExecutorService
JDK1.5之後 推出了線程池(ScheduledExecutorService),現階段定時任務與 JUC 包中的週期性線程池密不可分。JUC 包中的 Executor 架構帶來了線程的創建與執行的分離。Executor 的繼承者 ExecutorService 下面衍生出了兩個重要的實現類,他們分別是:
- ThreadPoolExecutor 線程池
- ScheduledThreadPoolExecutor 支持週期性任務的線程池
圖9 ScheduledExecutorService實現類圖
通過 ThreadPoolExecutor 可以實現各式各樣的自定義線程池,而 ScheduledThreadPoolExecutor 類則在自定義線程池的基礎上增加了週期性執行任務的功能。
-
最大線程數爲Integer.MAX_VALUE;表明線程池內線程數不受限制:即這是因爲延遲隊列內用數組存放任務,數組初始長度爲16,但數組長度會隨着任務數的增加而動態擴容,直到數組長度爲Integer.MAX_VALUE;既然隊列能存放Integer.MAX_VALUE個任務,又因爲任務是延遲任務,因此保證任務不被拋棄,最多需要Integer.MAX_VALUE個線程。
-
空閒線程的等待時間都爲0納秒,表明池內不存在空閒線程,除了核心線程:採用leader-follwer,這裏等待的線程都爲空閒線程,爲了避免過多的線程浪費資源,所以ScheduledThreadPool線程池內更多的存活的是核心線程。
-
任務等待隊列爲DelayedWorkQueue。
圖10 ScheduledThreadPoolExecutor中的延時隊列DelayedWorkQueue
總結: ScheduledThreadPoolExecutor中定義內部類ScheduledFutureTask、DelayedWorkQueue;ScheduledFutureTask記錄任務定時信息,DelayedWorkQueue來排序任務定時執行。ScheduledExecutorService自定義了阻塞隊列DelayedWorkQueue給線程池使用,它可以根據ScheduledFutureTask的下次執行時間來阻塞take方法,並且新進來的ScheduledFutureTask會根據這個時間來進行排序,最小的最前面。
-
DelayedWorkQueue:其中DelayedWorkQueue是定義的延時隊列,可以看做是一個用延時時間長短作爲排序的優先級隊列,來實現加入任務,DelayedWorkQueue原理見3.1.3;
-
ScheduledFutureTask是用作實現Run方法,使得任務能夠延遲執行,甚至週期執行,並且記錄每個任務進入延時隊列的序列號sequenceNumber。任務類ScheduledFutureTask繼承FutureTask並擴展了一些屬性來記錄任務下次執行時間和每次執行間隔。同時重寫了run方法重新計算任務下次執行時間,並把任務放到線程池隊列中。
run()在處理任務時,會根據任務是否是週期任務走不通的流程:
- 非週期任務,則採用futureTask類的run()方法,不存儲優先隊列;
- 週期任務,首先確定任務的延遲時間,然後把延遲任務插入優先隊列;
ScheduledFutureTask的reExecutePeriodic(outerTask)方法:把週期任務插入優先隊列的過程。
3.2.2 實現SchedulingConfigurer接口
Spring Boot 提供了一個 SchedulingConfigurer 配置接口。我們通過 ScheduleConfig 配置文件實現 ScheduleConfiguration 接口,並重寫 configureTasks() 方法,向 ScheduledTaskRegistrar 註冊一個 ThreadPoolTaskScheduler 任務線程對象即可。
圖11 任務調度配置接口
3.2.3 Java任務調度框架Quartz
圖12 Quartz任務調度框架
-
Job:定義需要執行的任務,該類是一個接口,只定義了一個方法execute(JobExecutionContext context),在實現類的execute方法中編寫所需要定時執行的Job(任務),Job運行時的信息保存在JobDataMap實例中。
-
Trigger:負責設置調度策略。該類是一個接口,描述觸發job執行的時間觸發規則。主要有SimpleTrigger和CronTrigger這兩個子類。當且僅當需調度一次或者以固定時間間隔週期執行調度,SimpleTrigger 是最適合的選擇;而CronTrigger則可以通過Cron表達式定義出各種複雜時間規則的調度方案:如在週一到週五的15:00 ~ 16:00 執行調度等。
-
Scheduler:調度器就相當於一個容器,裝載着任務和觸發器。該類是一個接口。代表一個Quartz的獨立運行容器。Trigger和JobDetail可以註冊到Scheduler中,兩者在Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查找定位容器中某一對象的依據。
-
JobDetail:描述Job的實現類及其它相關的靜態信息,如:Job名字、描述、關聯監聽器等信息。Quartz每次調度Job時,都重新創建一個Job實例,它接受一個Job實現類,以便運行時通過newInstance()的反射機制實例化Job。
-
ThreadPool Scheduler使用一個線程池作爲任務運行的基礎設施,任務通過共享線程池中的線程提高運行效率。
-
Listener:Quartz擁有完善的事件和監聽體系,大部分組件都擁有事件,如:JobListener監聽任務執行前事件、任務執行後事件;TriggerListener監聽觸發前事件,出發後事件;TriggerListener監聽調度開始事件,關閉事件等等,可以註冊響應的監聽器處理感興趣的事件。
針對Quartz 重複調度問題:
在通常的情況下,樂觀鎖能保證不發生重複調度,但是難免發生ABA問題。
配置文件加上:org.quartz.jobStore.acquireTriggersWithinLock=true
3.2.4 使用 Spring-Task
如果使用的是 Spring 或 Spring Boot 框架,Spring 作爲一站式框架,爲開發者提供了異步執行和任務調度的抽象接口TaskExecutor 和TaskScheduler。
-
Spring TaskExecutor:主要用來創建線程池用來管理異步定時任務開啓的線程。(防止建立線程過多導致資源浪費)。
-
Spring TaskScheduler:創建定時任務
其中Spring自帶的定時任務工具,spring task,可以將它比作一個輕量級的Quartz,而且使用起來很簡單,除spring相關的包外不需要額外的包,而且支持註解和配置文件兩種:
使用方法:
-
聲明開啓 Scheduled:通過註解 @EnableScheduling或者配置文件
-
任務方法添加@Scheduled註解
-
將任務的類交結 Spring 管理 (例如使用 @Component)
圖13 定時任務配置文件和cron表達式
圖14 @Scheduled註解攔截類ScheduledAnnotationBeanPostProcessor的實現類圖
類圖簡要介紹:
- 實現感知接口:EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware;
- 在spring啓動完成單例bean注入,利用接口MergedBeanDefinitionPostProcessor完成掃描,利用BeanPostProcessor接口中的postProcessAfterInitialization掃描被@Scheduled註解表示的方法;
- 利用ScheduledTaskRegistrar作爲註冊中心,監聽到所有bean注入完成之後,然後開始註冊全部任務;
- 自定義任務調度器TaskScheduler,默認使用接口ScheduledExecutorService的實現類ScheduledThreadPoolExecutor定義單線程的線程池。
@Scheduler註解源碼:
【Java】
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
//這個變量在ScheduledAnnotationBeanPostProcessor中會用作開啓cron的判斷條件
String cron() default "";
//用於設置類cron表達式 來描述人物的運行時機
String zone() default "";
//用於設置任務的上一次調用結束後到下一次調用開始前的時間間隔,單位:毫秒
long fixedDelay() default -1L;
//參數 fixedDelay 的字符串參數形式,與fixedDelay只能二選一使用
String fixedDelayString() default "";
//用於設置任務的兩次調用之間的固定的時間間隔,單位:毫秒
long fixedRate() default -1L;
//參數 fixedRate 的字符串參數形式,與fixedRate只能二選一使用
String fixedRateString() default "";
//用於設置在首次執行fixedDelay或fixedRate任務之前要延遲的毫秒數
long initialDelay() default -1L;
//參數 initialDelay 的字符串參數形式,與initialDelay只能二選一使用
String initialDelayString() default "";
}
項目啓動時,在初始化 bean 後,帶 @Scheduled 註解的方法會被攔截,然後依次:構建執行線程,解析參數,加入線程池。其中作爲攔截註解的類就是ScheduledAnnotationBeanPostProcessor。
ScheduledAnnotationBeanPostProcessor攔截類中核心註解處理方法:
【Java】
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
//校驗此註解的方法必領是無參的方法
//包裝返回一個Runnable線程
Runnable runnable = this.createRunnable(bean, method);
//定義一個校驗註冊任務最終是否執行的標識
boolean processedSchedule = false;
//裝載任務,定義爲4,主要涉及的註解也就3個
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
Set<ScheduledTask> tasks = new LinkedHashSet(4);
//long和string二者取其一
long initialDelay = scheduled.initialDelay();
String initialDelayString = scheduled.initialDelayString();
if (StringUtils.hasText(initialDelayString)) {
Assert.isTrue(initialDelay < 0L, "Specify 'initialDelay' or 'initialDelayString', not both");
if (this.embeddedValueResolver != null) {
initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
}
if (StringUtils.hasLength(initialDelayString)) {
try {
initialDelay = parseDelayAsLong(initialDelayString);
} catch (RuntimeException var24) {
throw new IllegalArgumentException("Invalid initialDelayString value "" + initialDelayString + "" - cannot parse into long");
}
}
}
// Check cron expression
// 解析cron
// cron也可以使用佔位符。把它配置子啊配置文件裏就成
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
String zone = scheduled.zone();
if (this.embeddedValueResolver != null) {
cron = this.embeddedValueResolver.resolveStringValue(cron);
zone = this.embeddedValueResolver.resolveStringValue(zone);
}
if (StringUtils.hasLength(cron)) {
Assert.isTrue(initialDelay == -1L, "'initialDelay' not supported for cron triggers");
processedSchedule = true;
if (!"-".equals(cron)) {
TimeZone timeZone;
if (StringUtils.hasText(zone)) {
timeZone = StringUtils.parseTimeZoneString(zone);
} else {
timeZone = TimeZone.getDefault();
}
//如果配置了cron,那麼就可以看作是一個task了,就可以把任務註冊進registrar裏面
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
}
}
if (initialDelay < 0L) {
initialDelay = 0L;
}
long fixedDelay = scheduled.fixedDelay();
......
......
......
long fixedRate = scheduled.fixedRate();
......
......
......
//校驗註冊任務最終是否成功
Assert.isTrue(processedSchedule, errorMessage);
//最後把這些任務都放在全局屬性裏面保存起來
//getScheduledTasks()方法是會把所有的任務都返回出去
synchronized(this.scheduledTasks) {
Set<ScheduledTask> regTasks = (Set)this.scheduledTasks.computeIfAbsent(bean, (key) -> {
return new LinkedHashSet(4);
});
regTasks.addAll(tasks);
}
} catch (IllegalArgumentException var25) {
throw new IllegalStateException("Encountered invalid @Scheduled method '" + method.getName() + "': " + var25.getMessage());
}
返回所有的任務,該註冊類實現了ScheduledTaskHolder的方法。
返回所有的實現ScheduledTaskHolder的任務:
【Java】
public Set<ScheduledTask> getScheduledTasks() {
Set<ScheduledTask> result = new LinkedHashSet();
synchronized(this.scheduledTasks) {
Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values();
Iterator var4 = allTasks.iterator();
while(true) {
if (!var4.hasNext()) {
break;
}
Set<ScheduledTask> tasks = (Set)var4.next();
result.addAll(tasks);
}
}
result.addAll(this.registrar.getScheduledTasks());
return result;
}
ScheduledTaskRegistrar
ScheduledTask註冊中心,ScheduledTaskHolder接口的一個重要的實現類,維護了程序中所有配置的ScheduledTask。指定TaskScheduler或者ScheduledExecutorService都是ok的,ConcurrentTaskScheduler也是一個TaskScheduler的實現類。它是ScheduledAnnotationBeanPostProcessor的一個重要角色。
指定任務調度taskScheduler:
【Java】
//這裏,如果你指定的是一個TaskScheduler、ScheduledExecutorService皆可
//ConcurrentTaskScheduler也是一個TaskScheduler的實現類
public void setScheduler(@Nullable Object scheduler) {
if (scheduler == null) {
this.taskScheduler = null;
} else if (scheduler instanceof TaskScheduler) {
this.taskScheduler = (TaskScheduler)scheduler;
} else {
if (!(scheduler instanceof ScheduledExecutorService)) {
throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
}
this.taskScheduler = new ConcurrentTaskScheduler((ScheduledExecutorService)scheduler);
}
}
重要的一步:如果沒有指定taskScheduler ,這裏面會new一個newSingleThreadScheduledExecutor,但它並不是一個合理的線程池,所以所有的任務還需要One by One順序執行,其中默認爲:Executors.newSingleThreadScheduledExecutor(),所以肯定單線程串行執行。
觸發執行定時任務:
【Java】
protected void scheduleTasks() {
//這一步非常重要:如果我們沒有指定taskScheduler,這裏會new一個newSingleThreadScheduledExecutor
//顯然他並不是一個真的線程池,所以他所有的任務還是得一個一個的執行
//默認是Executors.newSingleThreadScheduledExecutor(),所以必定是串行執行
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
//下面就是藉助TaskScheduler來啓動每一個任務
//並且把啓動了的任務最終保存到scheduleTasks中
Iterator var1;
if (this.triggerTasks != null) {
var1 = this.triggerTasks.iterator();
while(var1.hasNext()) {
TriggerTask task = (TriggerTask)var1.next();
this.addScheduledTask(this.scheduleTriggerTask(task));
}
}
......
......
......
}
四、分佈式定時任務
上面的方法都是關於單機定時任務的實現,如果是分佈式環境可以使用 Redis 來實現定時任務。
使用 Redis 實現延遲任務的方法大體可分爲兩類:通過 ZSet 的方式和鍵空間通知的方式。
4.1 通過 ZSet 的方式、Redis 的鍵空間通知
上述方案都是基於單線程的任務調度,如何引入多線程提高延時任務的併發處理能力?
-
通過 ZSet 實現定時任務的思路是,將定時任務存放到 ZSet 集合中,並且將過期時間存儲到 ZSet 的 Score 字段中,然後通過一個無線循環來判斷當前時間內是否有需要執行的定時任務,如果有則進行執行。
-
可以通過 Redis 的鍵空間通知來實現定時任務,它的實現思路是給所有的定時任務設置一個過期時間,等到過期之後通過訂閱過期消息就能感知到定時任務需要被執行了,此時執行定時任務即可。
-
默認情況下 Redis 是不開啓鍵空間通知的,需要通過 config set notify-keyspace-events Ex 的命令手動開啓。
4.2 Elastic-job、xxl-job
-
elastic-job:是由噹噹網基於quartz 二次開發之後的分佈式調度解決方案 , 由兩個相對獨立的子項目Elastic-Job-Lite和Elastic-Job-Cloud組成 。elastic-job主要的設計理念是無中心化的分佈式定時調度框架,思路來源於Quartz的基於數據庫的高可用方案。但數據庫沒有分佈式協調功能,所以在高可用方案的基礎上增加了彈性擴容和數據分片的思路,以便於更大限度的利用分佈式服務器的資源。
-
XXL-JOB:是一個輕量級分佈式任務調度框架,它的核心設計理念是把任務調度分爲兩個核心部分:調度中心(xxl-admin),和執行器。隔離成兩個部分。這是一種中心化的設計,由調度中心來統一管理和調度各個接入的業務模塊(也叫執行器),接入的業務模塊(執行器)只需要接收調度信號,然後去執行具體的業務邏輯,兩者可以各自的進行擴容。
五、總結
定時任務作爲業務場景中不可或缺的一種通用能力,運用適合的定時任務能夠快速解決業務問題,同時又能避免過度設計帶來的資源浪費。本文旨在梳理目前定時任務的主流方案設計和原理,希望在讀者在技術選型和方案重構時有所幫助,唯有落地推動業務的技術纔有價值。技術永遠不停變革,思考不能止步不前。
作者:京東物流 肖明睿
來源:京東雲開發者社區