日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

【51CTO.com原創(chuàng)稿件】今天想跟大家一起探討一個聽起來很簡單的話題:定時任務(wù)機(jī)制。

老板喊你設(shè)計一個高效的定時任務(wù)系統(tǒng)

圖片來自 Pexels

無非就是一個計時器,到了指定時間就開始跑唄。too young,要是這么簡單我還說啥呢,干不就完了。

那如果是幾千上萬個定時任務(wù),你的計時器該如何設(shè)計呢?如果是 A 任務(wù)執(zhí)行完后再執(zhí)行 B 任務(wù)你會怎么調(diào)度呢?

如果是幾十臺機(jī)器同時要處理一些任務(wù),你又該如何設(shè)計呢?帶著這些看似不簡單的問題我們開始時間之旅。

操作系統(tǒng)的時間系統(tǒng)

應(yīng)用程序部署在操作系統(tǒng)上,定時任務(wù)依賴操作系統(tǒng)的時鐘。鑒于大部分的服務(wù)器都部署在 linux 上,我們就只討論 Linux 的時間系統(tǒng),windows 服務(wù)器別打我。

大部分 PC 機(jī)中有兩個時鐘源,他們分別叫做 RTC(Real Time Clock,實(shí)時時鐘) 和 OS(操作系統(tǒng))時鐘。

RTC

RTC(Real Time Clock,實(shí)時時鐘)也叫做 CMOS 時鐘,它是 PC 主機(jī)板上的一塊芯片(或者叫做時鐘電路),它靠電池供電,即使系統(tǒng)斷電也可以維持日期和時間。

由于獨(dú)立于操作系統(tǒng)所以也被稱為硬件時鐘,它為整個計算機(jī)提供一個計時標(biāo)準(zhǔn),是最原始最底層的時鐘數(shù)據(jù)。

OS 時鐘

OS 時鐘產(chǎn)生于 PC 主板上的定時/計數(shù)芯片(8253/8254),由操作系統(tǒng)控制這個芯片的工作,OS 時鐘的基本單位就是該芯片的計數(shù)周期。

在開機(jī)時操作系統(tǒng)取得 RTC 中的時間數(shù)據(jù)來初始化 OS 時鐘,然后通過計數(shù)芯片的向下計數(shù)形成了 OS 時鐘,所以 OS 時鐘并不是本質(zhì)意義上的時鐘,它更應(yīng)該被稱為一個計數(shù)器。

OS 時鐘只在開機(jī)時才有效,而且完全由操作系統(tǒng)控制,所以也被稱為軟時鐘或系統(tǒng)時鐘。

時鐘中斷

Linux 的 OS 時鐘的物理產(chǎn)生原因是可編程定時/計數(shù)器產(chǎn)生的輸出脈沖,這個脈沖送入 CPU,就可以引發(fā)一個中斷請求信號,我們就把它叫做時鐘中斷。

Linux 中用全局變量 jiffies 表示系統(tǒng)自啟動以來的時鐘滴答數(shù)目。每個時鐘滴答,時鐘中斷得到執(zhí)行。

時鐘中斷執(zhí)行的頻率很高:100 次/秒(Linux 設(shè)計者將一個時鐘滴答(tick)定義為 10ms),時鐘中斷的主要工作是處理和時間有關(guān)的所有信息、決定是否執(zhí)行調(diào)度程序。

和時間有關(guān)的所有信息包括系統(tǒng)時間、進(jìn)程的時間片、延時、使用 CPU 的時間、各種定時器,進(jìn)程更新后的時間片為進(jìn)程調(diào)度提供依據(jù),然后在時鐘中斷返回時決定是否要執(zhí)行調(diào)度程序。

在單處理器系統(tǒng)中,每個 tick 只發(fā)生一次時鐘中斷。在對應(yīng)的中斷處理程序中完成更新系統(tǒng)時間、統(tǒng)計、定時器、等全部功能。

而在多處理器系統(tǒng)下,時鐘中斷實(shí)際上是分成兩個部分:

  • 全局時鐘中斷,系統(tǒng)中每個 tick 只發(fā)生一次。對應(yīng)的中斷處理程序用于更新系統(tǒng)時間和統(tǒng)計系統(tǒng)負(fù)載。
  • 本地時鐘中斷,系統(tǒng)中每個 tick 在每個 CPU 上發(fā)生一次。對應(yīng)的中斷處理程序用于統(tǒng)計對應(yīng) CPU 和運(yùn)行于該CPU上的進(jìn)程的時間,以及觸發(fā)對應(yīng) CPU 上的定時器。

于是,在多處理器系統(tǒng)下,每個 tick,每個 CPU 要處理一次本地時鐘中斷;另外,其中一個 CPU 還要處理一次全局時鐘中斷。

時鐘中斷的應(yīng)用

更新系統(tǒng)時間:在 Linux 內(nèi)核中,全局變量 jiffies_64 用于記錄系統(tǒng)啟動以來所經(jīng)歷的 tick 數(shù)。

每次進(jìn)入時鐘中斷處理程序(多處理器系統(tǒng)下對應(yīng)的是全局時鐘中斷)都會更新 jiffies_64 的值,正常情況下,每次總是給 jiffies_64 加 1。

而時鐘中斷存在丟失的可能。內(nèi)核中的某些臨界區(qū)是不能被中斷的,所以進(jìn)入臨界區(qū)前需要屏蔽中斷。

當(dāng)中斷屏蔽取消的時候,硬件只能告訴內(nèi)核是否曾經(jīng)發(fā)生了時鐘中斷、卻不知道已經(jīng)發(fā)生過多少次。

于是,在極端情況下,中斷屏蔽時間可能超過 1 個 tick,從而導(dǎo)致時鐘中斷丟失。

如果計算機(jī)上的時鐘振蕩器有很高的精度,Linux 內(nèi)核可以讀振蕩器中的計數(shù)器,通過比較上一次讀的值與當(dāng)前值,以確定中斷是否丟失;如果發(fā)現(xiàn)中斷丟失,則本次中斷處理程序會給 jiffies_64 增加相應(yīng)的計數(shù)。

但是如果振蕩器硬件不允許(不提供計數(shù)器、或者計數(shù)器不允許讀、或者精度不夠),內(nèi)核也沒法知道時鐘中斷是否丟失了。

內(nèi)核中的全局變量 xtime 用于記錄當(dāng)前時間(自 1970-01-01 起經(jīng)歷的秒數(shù)、本秒中經(jīng)歷的納秒數(shù))。xtime 的初始值就是內(nèi)核啟動時從 RTC 讀出的。

在時鐘中斷處理程序更新 jiffies_64 的值后,便更新 xtime 的值。通過比較 jiffies_64 的當(dāng)前值與上一次的值(上面說到,差值可能大于 1),決定 xtime 應(yīng)該更新多少。

系統(tǒng)調(diào)用 gettimeofday(對應(yīng)庫函數(shù) time 和 gettimeofday)就是用來讀 xtime 變量的,從而讓用戶程序獲取系統(tǒng)時間。

實(shí)現(xiàn)定時器:既然已知每個 tick 是 10ms,用 tick 來做定時任務(wù)統(tǒng)計再好不過。無論是內(nèi)核還是應(yīng)用系統(tǒng)其實(shí)都有大量的定時任務(wù)需求,這些定時任務(wù)類型不一,但是都是依賴于 tick。

已知的操作系統(tǒng)實(shí)現(xiàn)定時任務(wù)的方式有哪些呢?

①維護(hù)一個帶過期時間的任務(wù)鏈表

簡單且行之有效的方式。在一個全局鏈表中維護(hù)一個定時任務(wù)鏈。每次 tick 中斷來臨,遍歷該鏈表找到 expire 到期的任務(wù)。

如果將任務(wù)以 expire 排序,每次只用找到鏈頭的元素即可,時間復(fù)雜度為 O(1)。

這種方式對于早期的 Linux 系統(tǒng)來說沒有問題,隨著現(xiàn)在的系統(tǒng)復(fù)雜度漸漸變化,它無法支撐如今的網(wǎng)絡(luò)流量暴增時代的需求。

②時間輪(Timing-Wheel)算法

老板喊你設(shè)計一個高效的定時任務(wù)系統(tǒng)

 

時間輪很容易理解,上圖有 n 個 bucket,每一個 bucket 表示一秒,當(dāng)前 bucket 表示當(dāng)前這一秒到來之后要觸發(fā)的事件。

每個 bucket 會對應(yīng)著一個鏈表,鏈表中存儲的就是當(dāng)前時刻到來要處理的事件。

那這里有個問題來了,如果有個定時任務(wù)需要在 16 小時后執(zhí)行,換算成秒就是 57600s,難道我們的時間輪也要這么多個 bucket 嗎。幾萬個對內(nèi)存也是一種損耗。

為了減少 bucket 的數(shù)量,時間輪算法提供了一個擴(kuò)展算法,即 Hierarchy 時間輪。

老板喊你設(shè)計一個高效的定時任務(wù)系統(tǒng)

 

Hierarchy 很好理解,層級制度。既然一個時間輪可能會導(dǎo)致 bucket 過多,那么為什么不能多弄幾個輪子來代替時分秒呢?

基于時、分、秒各自實(shí)現(xiàn)一個 wheel,每個 wheel 維護(hù)一個自己的 cursor,在 Hour 數(shù)組中,每個 bucket 代表一個小時。

Minute 數(shù)組中每一個 bucket 代表 1 分鐘,Second 數(shù)組中每個 bucket 代表 1 秒。

采用分層時間輪,我們只需要 24+60+60=144 個 bucket 就可以表示所有的時間。

完全模擬到時鐘的用法,Second wheel 每轉(zhuǎn)完 60 個 bucket ,要聯(lián)動 Minute wheel 轉(zhuǎn)動一格,同理 Minite wheel 轉(zhuǎn)動 60 個 bucket 也要聯(lián)動 Hours wheel 轉(zhuǎn)動一格。

③維護(hù)一個基于小根堆算法的定時任務(wù)

小根堆的性質(zhì)是滿足除了根節(jié)點(diǎn)以外的每個節(jié)點(diǎn)都不小于其父節(jié)點(diǎn)的堆。基于這種性質(zhì)從根節(jié)點(diǎn)開始遍歷每個節(jié)點(diǎn)能保證獲取到一個最小優(yōu)先級的隊列。

那么應(yīng)用到定時器中,每次只用獲取當(dāng)前最小堆的 root 節(jié)點(diǎn)看是否到期即可。最小堆的插入時間復(fù)雜度為 O(lgn),獲取頭結(jié)點(diǎn)時間復(fù)雜度為 O(1)。

開箱即用的定時器

單機(jī)版定時器

①cron/crontab

cron 是 Linux 中的一個定時任務(wù)機(jī)制。cron 表示一個在后臺運(yùn)行的守護(hù)進(jìn)程,crontab 是一個設(shè)置 cron 的工具,所有的定時任務(wù)都寫在 crontab 文件中。

cron 調(diào)度的是 /etc/crontab 文件中的內(nèi)容。crontab 的命令構(gòu)成為時間+動作,其時間有分、時、日、月、周五種。

這里要注意,最小單位為分鐘,默認(rèn)是不到秒的級別,大家也給出了各種精確到秒的方案,有興趣的可以搜索一下。

/etc/crontab 文件中的每一行都代表一項任務(wù),它的格式是:

minute    hour      day       month      dayofweek      user-name      command 
* minute — 分鐘,從 0 到 59 之間的任何整數(shù) 
* hour — 小時,從 0 到 23 之間的任何整數(shù) 
* day — 日期,從 1 到 31 之間的任何整數(shù)(如果指定了月份,必須是該月份的有效日期) 
* month — 月份,從 1 到 12 之間的任何整數(shù)(或使用月份的英文簡寫如 jan、feb 等等) 
* dayofweek — 星期,從 0 到 7 之間的任何整數(shù),這里的 0 或 7 代表星期日(或使用星期的英文簡寫如 sun、mon 等等) 
* user-name - 用戶,腳本以什么用戶執(zhí)行 
* command — 要執(zhí)行的命令(命令可以是 ls /proc >> /tmp/proc 之類的命令,也可以是執(zhí)行你自行編寫的腳本的命令。) 

②JDK 提供的定時器:Timer

Timer 的思路很簡單,基于最小堆的方案創(chuàng)建一個 TaskQueue 來盛 TimerTask。

Timer 中有一個 TimerThread 線程,該線程是 Timer 中唯一負(fù)責(zé)任務(wù)輪詢和任務(wù)執(zhí)行的線程。

這就意味著如果一個任務(wù)耗時很久,久到已經(jīng)超過了下個任務(wù)的開始執(zhí)行時間,那么就意味下一個任務(wù)會延遲執(zhí)行。

另外 Timer 線程是不會捕獲異常的,如果某個 TimerTask 執(zhí)行過程中發(fā)生了異常而被終止,那么后面的任務(wù)將不會被執(zhí)行。所以要做好異常處理防止出現(xiàn)異常影響任務(wù)繼續(xù)。

因?yàn)橛凶枞彤惓=K止的缺點(diǎn),JDK 又封裝了另一個定時器的實(shí)現(xiàn)方式,這次保證不會阻塞。

因?yàn)樗蔷€程池實(shí)現(xiàn)方式的一種:ScheduledExecutorService。ScheduledExecutorService 內(nèi)部將任務(wù)封裝之后交給了 DelayQueue。

DelayQueue 是一個依靠 AQS 隊列同步器所實(shí)現(xiàn)的無界延遲阻塞隊列,內(nèi)部通過 PriorityQueue 來實(shí)現(xiàn),本質(zhì)還是還是一個堆,所以插入的時間復(fù)雜度也是 O(lgn)。

③Netty 封裝的時間輪:HashedWheelTimer

上面簡要描述了操作系統(tǒng)中的時間輪實(shí)現(xiàn),在著名框架 Netty 中也封裝了一個自己的時間輪實(shí)現(xiàn):HashedWheelTimer 類。

因?yàn)?Netty 中需要管理大量的 I/O 超時事件,基于時間輪的方案有助于節(jié)省資源。

Netty 中采用一個輪子的方案,一個格子代表的時間是 100ms,默認(rèn)有 512 個格子。

來看看 HashedWheelTimer 的構(gòu)造函數(shù)參數(shù):

HashedWheelTimer( 
    ThreadFactory threadFactory, //類似于Clock中的updater, 負(fù)責(zé)創(chuàng)建Worker線程. 
    long tickDuration,           //時間刻度之間的時長(默認(rèn)100ms), 通俗的說, 就是多久tick++一次. 
    TimeUnit unit,               //tickDuration的單位. 
    int ticksPerWheel            //類似于Clock中的wheel的長度(默認(rèn)512). 
); 

另外為了不無休止的增加 bucket,這里采用了輪(round)的概念,一輪所花費(fèi)的時間:round time=ticksPerWheel*tickDuration。

如果 bucket 只有 512 個, 而當(dāng)前休眠時間長于一輪,那么就增加相應(yīng)的輪次來表示當(dāng)前休眠時長。

HashedWheelTimer 中有一些主要的成員:

  • HashedWheelTimer 類本身,主要負(fù)責(zé)啟動 Worker 線程、添加任務(wù)等。
  • Worker:內(nèi)部負(fù)責(zé)添加任務(wù),累加 tick,執(zhí)行任務(wù)等。
  • HashedWheelTimeout:任務(wù)的包裝類,鏈表結(jié)構(gòu),負(fù)責(zé)保存 deadline,輪數(shù)等。
  • HashedWheelBucket:wheel 數(shù)組元素,負(fù)責(zé)存放 HashedWheelTimeout 鏈表。

Worker 線程是 HashedWheelTimer 的核心,主要負(fù)責(zé)每當(dāng)已過 tickDuration 時間就累加一次 tick。

同時也負(fù)責(zé)執(zhí)行到期的 timeout 任務(wù)和添加 timeout 任務(wù)到指定的 wheel 中。

當(dāng)添加 Timeout 任務(wù)的時候,會根據(jù)設(shè)置的時間來計算出需要等待的時間長度,根據(jù)時間長度進(jìn)而算出要經(jīng)過多少次 tick,然后根據(jù) tick 的次數(shù)來算出經(jīng)過多少輪最終得出 task 在 wheel 中的位置。

對于這種時間輪一般是怎么遍歷判斷任務(wù)到期呢?每個 ticket 到來,都要去遍歷每一個 bucket ,以此來判斷是否有 bucket 到期。

所以這種方式就要求 bucket 盡量不要太多,如果太多每次遍歷都需要很長的時間。另外就是每次都會遍歷,必然會有很多空轉(zhuǎn),也是一種資源的浪費(fèi)。

④Kafka 中的時間輪:TimingWheel

Netty 中的時間輪實(shí)現(xiàn)采用了單輪+round 的模式,在 Kafka 中采用了多輪的模式。

上面說過多輪模式下如果按照時分秒來表達(dá),每個輪所需的 bucket 都非常的少,遍歷輪的時候就會很快。

但是多輪也會帶來另一個問題就是輪的維護(hù):比如有個定時任務(wù)是 1*60*60+50=36050s,這時候就需要分鐘和秒輪同時維護(hù)這個任務(wù)。

當(dāng)這個任務(wù)繼續(xù)走,只剩下 59s 的時候,分鐘輪就無需在維護(hù)它的信息,只剩下秒輪來維護(hù),這里出現(xiàn)了降輪的概念 。

單機(jī)定時機(jī)制對比

以上簡單描述了各個實(shí)現(xiàn)方案,簡單對比可以得出:

Timer 的實(shí)現(xiàn)方案毋庸置疑是最差的。阻塞,異常退出這兩條“罪名”無疑讓現(xiàn)代程序員無法承受因?yàn)槌鲥e被老板罵的鍋。

ScheduledExecutorService 使用線程池的方式來異步的執(zhí)行任務(wù),當(dāng)任務(wù)量巨大的時候,如果設(shè)置了優(yōu)先數(shù)量的可執(zhí)行線程,無疑還是會阻塞任務(wù),好在可執(zhí)行線程多。

而 HashedWheelTimer 是面向 bucket 設(shè)計,如果采用多輪的方式可以不受任務(wù)量限制,任務(wù)量非常大的時候,維護(hù)數(shù)組的成本遠(yuǎn)遠(yuǎn)要低于維護(hù)堆的成本。

但是如果是任務(wù)量很少的情況,時間輪依舊需要全盤掃描,出現(xiàn)空轉(zhuǎn)的狀態(tài),這種空載無疑也是浪費(fèi)資源的提現(xiàn)。

所以面向使用場景編程的話:

  • 如果當(dāng)前待運(yùn)行的定時任務(wù)屬于耗時長一點(diǎn),任務(wù)量也不是那么大的時候,可以采用 ScheduledExecutorService 的方式來實(shí)現(xiàn)。
  • 如果任務(wù)量比較大,任務(wù)耗時短,無疑使用 HashedWheelTimer 對內(nèi)存更加友好。

定時任務(wù)系統(tǒng)

前面從操作系統(tǒng)時鐘源開始,說到時鐘中斷產(chǎn)生了時鐘滴答,所有的定時任務(wù)都依賴于此。

軟件層面,通過各種有效的算法在節(jié)約資源的前提下通過監(jiān)聽時鐘滴答來實(shí)現(xiàn)任務(wù)。

還記得開篇提到我們本篇文章的意圖是什么嗎,要設(shè)計一個高效的定時任務(wù)系統(tǒng)。

既然談到了設(shè)計,是不是要先出一版產(chǎn)品需求文檔呢。這個真的可以有,我們先提提需求再聊聊方案。

你要的需求設(shè)計

定時任務(wù)系統(tǒng)的核心功能是什么?既然是第一版,我們不要那些花里胡哨,錦上添花的功能,從本質(zhì)出發(fā)。

老板喊你設(shè)計一個高效的定時任務(wù)系統(tǒng)

 

我理解應(yīng)該有三個核心模塊:

任務(wù)錄入:提供錄入定時任務(wù)的入口,支持最基本的定時任務(wù)機(jī)制:cron 表達(dá)式,自定義執(zhí)行時間等等方式。

任務(wù)調(diào)度:通過合適的調(diào)度算法從任務(wù)庫中觸發(fā)到期的任務(wù)以期執(zhí)行,當(dāng)然調(diào)度系統(tǒng)最好不要直接參數(shù)執(zhí)行,做好自己的事即可。

任務(wù)執(zhí)行:調(diào)度系統(tǒng)已經(jīng)觸發(fā)了任務(wù),那么可以由專門的執(zhí)行系統(tǒng)來負(fù)責(zé)任務(wù)執(zhí)行,執(zhí)行不會阻塞任務(wù)調(diào)度,縱然執(zhí)行有阻塞也是在執(zhí)行系統(tǒng)中阻塞,保持調(diào)度的可用性。

以上 3 個模塊就能滿足基本的任務(wù)系統(tǒng)需求,接下來聊聊實(shí)現(xiàn)方案。

技術(shù)實(shí)現(xiàn)方案

①錄入模塊實(shí)現(xiàn)

一般執(zhí)行定時任務(wù)的場景是:每隔多久執(zhí)行一次操作,這種在業(yè)務(wù)系統(tǒng)中最常見的就是使用 cron 表達(dá)式來代替,所以錄入模塊要做到可以解析 cron 表達(dá)式即可。

這種錄入模式主要是針對后臺手動錄入任務(wù)的場景,對于開發(fā)人員來說最優(yōu)解就是能用代碼實(shí)現(xiàn)就不去切換鼠標(biāo)(有同學(xué)說能點(diǎn)點(diǎn)鼠標(biāo)誰還去砌磚)。

所以還需要提供可執(zhí)行 jar 包用于業(yè)務(wù)系統(tǒng)集成,方便開發(fā)人員通過編碼的方式將任務(wù)錄入到系統(tǒng)。

總結(jié)一下錄入任務(wù)的兩種途徑:

  • 提供業(yè)務(wù)系統(tǒng)可集成 jar 包,由開發(fā)人員編碼錄入任務(wù)。
  • 提供管理后臺界面,提供可配置方式錄入任務(wù)。

對于業(yè)務(wù)代碼植入式的任務(wù)業(yè)務(wù)服務(wù)器啟動的時候會通過 jar 包把任務(wù)推送過來,對于后臺錄入的任務(wù)那就需要入庫保存。

②調(diào)度模塊實(shí)現(xiàn)

在拿到錄入模塊的定時任務(wù)配置信息之后接下來要做的事情:將 cron 表達(dá)式變?yōu)橐粋€個可執(zhí)行的時間點(diǎn)。

比如在 Spring 中就已經(jīng)提供解析 cron 的功能:CronSequenceGenerator 類可以幫我們執(zhí)行此操作。

有了可執(zhí)行時間點(diǎn)之后要做的事情就是管理它,讓它調(diào)度起來。上面我們討論過的各種調(diào)度算法此時可以派上用場。

如果任務(wù)密度不是很大,多為固定的定期執(zhí)行任務(wù),小根堆算法就可以勝任;如果任務(wù)密集,很多短期快速執(zhí)行的任務(wù),可以采用時間輪的方式提高效率。

另外,比如有個任務(wù)是 5 分鐘執(zhí)行一次,那么你一次要解析出來多少個可執(zhí)行的時間點(diǎn)?一天,一周,一個月?

這樣肯定是有問題的,目前的實(shí)現(xiàn)方案是任務(wù)首次啟動的時候給出第一次執(zhí)行的時間,每次執(zhí)行的時候去計算下次任務(wù)開始的時間。

這里有一個點(diǎn):JAVA 相關(guān)的框架現(xiàn)在實(shí)現(xiàn)的方案都是當(dāng)前任務(wù)執(zhí)行完成之后再計算下次任務(wù)開始執(zhí)行的時間。

如果任務(wù)是 5 分鐘一次,當(dāng)前時間是:10:00,第一個任務(wù)完成需要 6 分鐘,那么第二個任務(wù)開始的時間就是:

老板喊你設(shè)計一個高效的定時任務(wù)系統(tǒng)

 

我們預(yù)期是每隔 5 分鐘執(zhí)行一次,事實(shí)上除了第一次是按照預(yù)期的準(zhǔn)點(diǎn)執(zhí)行以外,后面都會在絕對時間上有延期。

到這里我們解決了兩個問題:

  • 解析時間表達(dá)式為時間點(diǎn),如何確認(rèn)周期性任務(wù)的下一個可執(zhí)行時間點(diǎn)。
  • 將可執(zhí)行時間點(diǎn)送入調(diào)度器中,讓時間流動起來。

③任務(wù)執(zhí)行模塊

任務(wù)錄入,任務(wù)調(diào)度我們都完成了,執(zhí)行模塊才是最后的重頭戲。這里我們再細(xì)化一下,任務(wù)錄入不能說只是把任務(wù)所屬的表達(dá)式載入系統(tǒng)就完事,要把任務(wù)對象化,達(dá)到招手即用的狀態(tài)。

這里我們把每個任務(wù)都封裝為一個對象 Job,所有的 Job 都在內(nèi)存中加載,調(diào)度器定義為 Scheduler,把每個可執(zhí)行時間封裝為 Trigger 對象。

Trigger 用于定義調(diào)度任務(wù)的事件規(guī)則,唯一關(guān)聯(lián)一個 Job 并標(biāo)識當(dāng)前 Job 的執(zhí)行狀態(tài)。

上圖就是我們的極簡版定時任務(wù)系統(tǒng)核心功能,怎么樣,麻雀雖小,五臟俱全。該有的功能一樣不少,不該有的功能一個都沒有。

老板喊你設(shè)計一個高效的定時任務(wù)系統(tǒng)

 

到這里為止我們已經(jīng)輸出了極簡版定時任務(wù)調(diào)度系統(tǒng)的核心設(shè)計和實(shí)現(xiàn)方案,依據(jù)這個方案你可以實(shí)現(xiàn)定時任務(wù)調(diào)度系統(tǒng)的單機(jī)版核心功能。

我們先不提加需求的問題,先來個高可用的問題,上面的方案是將任務(wù)加載到一臺機(jī)器的內(nèi)存中定時執(zhí)行,那么如果要實(shí)現(xiàn)高可用,多臺機(jī)器的情況任務(wù)如何防止多次執(zhí)行呢?

很顯然上面的方案肯定是行不通了,下面我們開始擴(kuò)容。

高可用

回答高可用的問題先說目前的思路:單機(jī)純內(nèi)存抗所有任務(wù)。要做高可用必然會大于等于 2 臺機(jī)器。

那么兩臺機(jī)器都執(zhí)行任務(wù)必然會重復(fù)運(yùn)行,該用什么方案在多機(jī)環(huán)境中可以統(tǒng)一管理,統(tǒng)一調(diào)度,統(tǒng)一運(yùn)行任務(wù)呢?

方案一:傳統(tǒng)方案-數(shù)據(jù)庫(獨(dú)占鎖)

任務(wù)觸發(fā)的關(guān)鍵在于 Trigger 觸發(fā)器,我們只用管住 Trigger 的手讓它別亂動 task 就好,基于數(shù)據(jù)庫操作的話,保證任一時刻某個 Trigger 只會被觸發(fā)一次即可。這里可以使用行級鎖來實(shí)現(xiàn)。

某臺機(jī)器執(zhí)行到這個 Trigger 的時候向數(shù)據(jù)庫插入一條 Trigger 記錄并持有該鎖,那么其余機(jī)器即使遇到了這個任務(wù)也不能執(zhí)行。

老板喊你設(shè)計一個高效的定時任務(wù)系統(tǒng)

 

方案二:分布式組件特性支持(分布式鎖)

一般來說數(shù)據(jù)庫肯定是值得信任的,但是面對實(shí)施要求高,任務(wù)執(zhí)行頻繁的場景的時候,數(shù)據(jù)庫又是不敢信任的,數(shù)據(jù)庫有一定的并發(fā)瓶頸。

要保證同一時刻的唯一性,除了數(shù)據(jù)庫的鎖特性以外,分布式組件肯定也支持,比如 Zookeeper,ETCD 等等。

可以利用 ZK 的臨時節(jié)點(diǎn)性質(zhì),同一個任務(wù)注冊一個唯一的節(jié)點(diǎn),哪個機(jī)器搶到這個節(jié)點(diǎn)誰就來執(zhí)行任務(wù)即可。

老板喊你設(shè)計一個高效的定時任務(wù)系統(tǒng)

 

產(chǎn)品加需求

基礎(chǔ)功能我們已經(jīng)完成,高可用也做到了,上線一段時間,產(chǎn)品覺得的整點(diǎn)幺蛾子啊,不然 KPI 咋整。

①新增功能

基于事件分發(fā)的任務(wù)機(jī)制:可能有一些任務(wù)是基于特定的條件觸發(fā),這種任務(wù)在分布式環(huán)境下一般自己實(shí)現(xiàn)分布式鎖來實(shí)現(xiàn),那么任務(wù)系統(tǒng)既然提供分布式特性也可以實(shí)現(xiàn)分布式鎖的功能。

所以對于這一類任務(wù)完全可以交給任務(wù)系統(tǒng)來做,把它當(dāng)成一次性觸發(fā)的任務(wù)。

②新增特性

任務(wù)終止:如果某個任務(wù)因?yàn)闃I(yè)務(wù)需求不再執(zhí)行,那么是否可以不發(fā)布的條件下終止該任務(wù)呢?這個時候任務(wù)終止的功能就很重要,產(chǎn)品經(jīng)理暗暗自喜,老板加雞腿。

任務(wù)依賴:B 任務(wù)依賴 A 任務(wù)的結(jié)果才能執(zhí)行,所以要提供任務(wù)之間的級聯(lián)操作。

任務(wù)分片:如果我們有 3 臺執(zhí)行任務(wù)的機(jī)器,有 10 個每 5s 執(zhí)行一次的定時任務(wù),恰恰每個任務(wù)都打到第一臺機(jī)器。它累如黃牛的時候另外兩臺還在曬太陽這豈不是資源的浪費(fèi)嘛。

為了避免任務(wù)集中到某一臺機(jī)和提高資源利用率,我們需要一種將任務(wù)均衡分配到當(dāng)前所有可執(zhí)行機(jī)器的能力,這就是所謂的分片機(jī)制。

常用的分片算法有如下:

平均分配算法:

  • 如果有 3 個任務(wù)實(shí)例,分成 9 片,每個實(shí)例對應(yīng)到的分片就是:1=[1,2,3],2=[4,5,6],3=[7,8,9]。
  • 如果有 3 個任務(wù)實(shí)例,分成 8 片,每個實(shí)例對應(yīng)到的分片就是:1=[0,1,6],2=[2,3,7],3=[4,5]。
  • 如果有 3 個任務(wù)實(shí)例,分成 10 片,每個實(shí)例對應(yīng)到的分片就是:1=[0,1,2,9],2=[3,4,5],3=[6,7,8]。

根據(jù)作業(yè)名 hash 值決定根據(jù) IP 升序/降序算法:

  • 如果有 3 個任務(wù)實(shí)例分別為 1,2,3,作業(yè)名稱對應(yīng)的 hash 值如果為奇數(shù)就按照 IP 升序?qū)ふ覚C(jī)器執(zhí)行,作業(yè)名稱對應(yīng)的 hash 值如果為偶數(shù)就按照 IP 降序?qū)ふ覚C(jī)器執(zhí)行。這種算法最多要求最多只有兩個分片,即只有兩臺機(jī)器參與執(zhí)行。

輪詢算法:

  • 輪詢的原理就很簡單,基于可執(zhí)行機(jī)器依次執(zhí)行。

任務(wù)日志:日志功能肯定不可少,檢測任務(wù)執(zhí)行成功與否,任務(wù)執(zhí)行記錄、時長,統(tǒng)計任務(wù)系統(tǒng)每日任務(wù)量等等。

③新增容錯機(jī)制

容錯機(jī)制:任務(wù)執(zhí)行失敗,可能是任務(wù)本身邏輯問題,也可能是外部條件,所以可以設(shè)置一些容錯機(jī)制,給它一次重試的機(jī)會。

故障轉(zhuǎn)移:集群中如果某一臺機(jī)器發(fā)生了故障,它如果還在注冊中心注冊,那么任務(wù)會被該機(jī)器執(zhí)行,很顯然如果僅有失敗重試策略,那么這個任務(wù)永遠(yuǎn)都不會執(zhí)行成功。

首先需要心跳檢測機(jī)制,檢測活動機(jī)器是否健康;其次需要在重試失敗之后做任務(wù)轉(zhuǎn)移操作,防止多次失敗仍在同一臺機(jī)器吊死。

手動觸發(fā):如果萬不得已遇到任務(wù)沒有執(zhí)行到的情況 ,那么是否要提供手動觸發(fā)的機(jī)制呢?我想產(chǎn)品經(jīng)理這種人精肯定不想背鍋,所以你還是做吧!

后話

做完上面的功能之后,產(chǎn)品經(jīng)理躺在他的折疊床上打著呼嚕鼻子不時的還冒幾個泡安心的睡著了。

程序員小哥整苦逼的構(gòu)思這些功能該如何實(shí)現(xiàn),是給 3 天還是給 3 個月,一場人月神話即將上演。

目前圈子里比較流行的定時任務(wù)系統(tǒng)有 Quartz,XXLJob,Elastic Job 等,實(shí)現(xiàn)方式不會脫離上文描述的范圍。

這些都是程序員自己沒事?lián)v鼓的實(shí)用型系統(tǒng),有需求就有產(chǎn)出,有方向就有動力。

工作之余大家也可以自己思考目前在寫的東西是否可以抽象為大層次的一個功能,簡單說,你是否也能整出個中臺來。

在這個萬物皆中臺的時代,大家不遺余力的照虎畫瓢,雖說可能畫出個四不像,起碼對于寫代碼的人,抽象能力是得到了鍛煉。

作者:楊越

簡介:目前就職廣州歡聚時代,專注音視頻服務(wù)端技術(shù),對音視頻編解碼技術(shù)有深入研究。日常主要研究怎么造輪子和維護(hù)已經(jīng)造過的輪子,深耕直播類 App 多年,對垂直直播玩法和應(yīng)用有廣泛的應(yīng)用經(jīng)驗(yàn),學(xué)習(xí)技術(shù)不局限于技術(shù),歡迎大家一起交流。

編輯:陶家龍

征稿:有投稿、尋求報道意向技術(shù)人請聯(lián)絡(luò) [email protected]

【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請注明原文作者和出處為51CTO.com】

分享到:
標(biāo)簽:定時 系統(tǒng)
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定