robfig/cron是GO語言中一個定時執行註冊任務的package, 最近我在工程中使用到了它,由於它的實現優雅且簡單(主要是簡單),所以將源碼過了一遍,記錄和分享在此。
文檔:http://godoc.org/github.com/robfig/cron,repo: https://github.com/robfig/cron
基本玩法
Demo代碼如下,先用cron.New()初始化一個實例,然後調用AddFunc(spec string, cmd func()) 註冊你希望調用的func,第一個參數為調度的時間策略,第二個參數為到時間後執行的方法。robfig/cron支持非常多樣的時間策略(下面的代碼舉了一些例子),最後通過cron.Start()方法啟動。
func TestCronDemo(t *testing.T) { c := cron.New() c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") }) c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") }) c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") }) c.AddFunc("@every 5m", func() { fmt.Println("every 5m, start 5m fron now") }) c.Start() c.Stop()}
type cronJobDemo int
func (c cronJobDemo) Run() { fmt.Println("5s func trigger") return}上面代碼中,第9、10行的代碼調用方法AddJob(spec string, cmd Job)也可以實現AddFunc註冊的功能,Job是interface,需要入參類型實現方法:Run()。實際上,方法AddFunc內部將參數cmd 進行了包裝(wrapper),然後也是調用方法AddJob進行註冊。
如果實際工程中定時執行的邏輯較為複雜,推薦使用方法AddJob()來註冊,自己寫方法Run(),這樣可以通過Run所屬的類型來傳遞所需數據,後面介紹都會說成AddJob,等效於AddFunc。
每當你用AddJob註冊一個定時調用策略,就會為這個策略生成一個唯一的Entry,不難想像,Entry裡會存儲被執行的時間、需要被調度執行的實體Job。
生成entry後,再將entry放到struct Cron的entry列表裡,Cron的結構裡,主要是一些用來和外部交互的channel,比如通過channel添加、刪除entry等。詳見下面的代碼。
type Entry struct { ID EntryID Schedule Schedule Next time.Time Prev time.Time WrappedJob Job Job Job}type Cron struct { entries []*Entry chain Chain stop chan struct{} add chan *Entry remove chan EntryID snapshot chan chan []Entry running bool logger Logger runningMu sync.Mutex location *time.Location parser ScheduleParser nextID EntryID jobWaiter sync.WaitGroup }需要注意的是,WrappedJob和chain這兩個成員,這是Cron實現的Job封裝邏輯,目前是解決實際調度Job的異常處理。比如你希望自己的上一個時間點的JobA沒有結束,下一個時間點的JobA就不執行,這個「不執行」的邏輯實現就定義在chain,初始化時通過chain將JobA進行封裝寫入WrappedJob,那麼每次JobA調用前會先執行封裝邏輯,進行判斷。
cron.Start()執行後,cron的後臺程序(方法run())就開始運行了。而它的主體,就是一個定時器的實現和到時後的job運行,加上cron裡的數據維護。
cron的定時器實現是一個簡潔而典型的業務層實現,著重了解下,具體
的流程圖可見下圖。
它的關鍵和值得學習之處是:
每個entry都包含自己下一次執行的絕對時間
先對entries按下次執行時間升序排序,只需要對第一個entry啟動定時器
定時器到時,只輪詢entries裡需要執行的entries,不需要全部輪詢。
且 執行的是當前時間之前的所有job,容錯高;
第一個定時器處理結束開啟下次定時器時,也只需要更新執行過的entries的下次執行時間,不需要更新所有的entries
上面的邏輯說完,程序主體已經清晰,除此之外,程序主體裡的定時器監聽和其他多個channel共用了select-case,這些channel在struct Cron裡能看到,實現了entries的動態添加、刪除、entries快照獲取等功能。代碼結構如下:
將這些操作通過channel讓程序主體來操作,可以有效的減少互斥鎖的使用,也會引入問題,會導致有的job執行時間不是非常精準,導致某些entry被遺漏,:
比如最近的jobA的timer在1ms後就要到時,此時加入一個entry,耗時3ms
添加完entry後,再重新啟動timer(還是jobA的timer,此處還利 用了golang的time.NewTimer(d Duration)的入參為負數會立即到時的特點)
下次到時的時間必然不是jobA期待的執行時間(理論上晚了2ms)
channel的操作首先是非常簡潔省時的,其次,定時器實現裡,會掃描所有當前時間之前的entries來執行,增加了容錯性
值得稱讚的細節
interface的使用
struct Entry裡的Schedule和Cron裡的ScheduleParser都是interface,意味著我們是可以自己定製註冊job時的時間策略的格式的,只要自己實現時間策略的解析和獲取方法就好
這讓我想起了以前看過golang裡什麼時候用interface和struct的討論,我覺得這是個很好的例子:預期對同一個接口有多個實現時就抽象成interface,不知道該不該用就用struct。
wrapper的實現
上面有提到,通過對Job的封裝,cron實現了同一個job多次調用時的異常處理等,值得以後在實踐中借鑑。
最後是我加了一點注釋的代碼
https://github.com/jiangz222/cron/tree/comments-v3
推薦閱讀
喜歡本文的朋友,歡迎關注「Go語言中文網」:
Go語言中文網啟用微信學習交流群,歡迎加微信:274768166,投稿亦歡迎