goCron是一個Golang作業調度工具,可以使用簡單的語法定期執行go函數。
github:https://github.com/jasonlvhit/gocron
api doc:https://godoc.org/github.com/jasonlvhit/gocron#Every
使用實例
package main
import (
"fmt"
"github.com/jasonlvhit/gocron"
)
func task() {
fmt.Println("I am runnning task.")
}
func taskWithParams(a int, b string) {
fmt.Println(a, b)
}
func main() {
//可併發運行多個任務
//注意 interval>1時調用sAPi
gocron.Every(2).Seconds().Do(task)
gocron.Every(1).Second().Do(taskWithParams, 1, "hi")
//在cron所有操作最後調用 start函數,否則start之後調用的操作無效不執行
//<-gocron.Start()
//在task執行過程中 禁止異常退出
gocron.Every(1).Minute().DoSafely(taskWithParams, 1, "hello")
// 支持在具體某一天、某天的某一時刻、每y-M-d h-m-s 執行任務
gocron.Every(1).Monday().Do(task)
gocron.Every(1).Thursday().Do(task)
// function At() take a string like 'hour:min'
gocron.Every(1).Day().At("10:30").Do(task)
gocron.Every(1).Monday().At("18:30").Do(task)
// 刪除某一任務
gocron.Remove(task)
//刪除所有任務
gocron.Clear()
//可同時創建一個新的任務調度 2個schedulers 同時執行
s := gocron.NewScheduler()
s.Every(3).Seconds().Do(task)
<-s.Start()
//防止多個集羣中任務同時執行 task 實現lock接口
//兩行代碼,對cron 設置lock實現,執行task時調用Lock方法再Do task
gocron.SetLocker(lockerImplementation)
gocron.Every(1).Hour().Lock().Do(task)
<-gocron.Start()
}
源碼淺析
輕量 簡潔的鏈式調用,看工程源碼,簡潔的……只有一個類。gocron當前支持最多1w個任務數,核心對象就是維護了job對象,所有執行的任務都放在jobs數組中,start方法底層調用go time包下的NewTicker,新啓一個線程執行task方法。
外部調用的gocron.start func調用鏈
// Start all the pending jobs
// Add seconds ticker
func (s *Scheduler) Start() chan bool {
stopped := make(chan bool, 1)
ticker := time.NewTicker(1 * time.Second)
go func() {
for {
select {
case <-ticker.C:
s.RunPending() //調用RunPending 執行數組有序的任務隊列
case <-stopped:
ticker.Stop()
return
}
}
}()
return stopped
}
// RunPending runs all the jobs that are scheduled to run.
func (s *Scheduler) RunPending() {
runnableJobs, n := s.getRunnableJobs()
if n != 0 {
for i := 0; i < n; i++ {
runnableJobs[i].run()
}
}
}
//run方法,反射獲取job的各屬性,最終調用function.call方法執行任務函數
//Run the job and immediately reschedule it
func (j *Job) run() (result []reflect.Value, err error) {
if j.lock {
if locker == nil {
err = fmt.Errorf("trying to lock %s with nil locker", j.jobFunc)
return
}
key := getFunctionKey(j.jobFunc)
if ok, err := locker.Lock(key); err != nil || !ok {
return nil, err
}
defer func() {
if e := locker.Unlock(key); e != nil {
err = e
}
}()
}
f := reflect.ValueOf(j.funcs[j.jobFunc])
params := j.fparams[j.jobFunc]
if len(params) != f.Type().NumIn() {
err = errors.New("the number of param is not adapted")
return
}
in := make([]reflect.Value, len(params))
for k, param := range params {
in[k] = reflect.ValueOf(param)
}
result = f.Call(in)
j.lastRun = time.Now()
j.scheduleNextRun()
return
}
這裏需要關注一下,gocron對lock的實現,從代碼上看Job結構體的lock屬性,用於控制多實例job併發執行。但項目woner提到的 multiple instances 指的並不是跨服務器的多實例,而是在同一應用服務 裏的多任務實例(也就是1個app服務中多個任務,粒度是隻在統一應用內)。如果跨server則lock需要自行依賴redis或其他分佈式鎖來管理。通過讀源碼的run方法,j.lock來控制job併發,但一旦跨server job.lock屬性是沒法共享的。這裏doc上給的解釋有點歧義,需要注意。
If you need to prevent a job from running at the same time from multiple cron instances (like running a cron app from multiple servers), you can provide a Locker implementation and lock the required jobs. 然後owner給出了一個基於redis來做的lock
Job結構體
// Job struct keeping information about job
type Job struct {
interval uint64 // pause interval * unit bettween runs
jobFunc string // the job jobFunc to run, func[jobFunc]
unit string // time units, ,e.g. 'minutes', 'hours'...
atTime time.Duration // optional time at which this job runs
lastRun time.Time // datetime of last run
nextRun time.Time // datetime of next run
startDay time.Weekday // Specific day of the week to start on
funcs map[string]interface{} // Map for the function task store
fparams map[string][]interface{} // Map for function and params of function
lock bool // lock the job from running at same time form multiple instances
}
scheduler內維護JOBs array數組和大小,gocron scheduler最大可執行1w個job(大小可重寫)。
// Scheduler struct, the only data member is the list of jobs.
// - implements the sort.Interface{} for sorting jobs, by the time nextRun
type Scheduler struct {
jobs [MAXJOBNUM]*Job // Array store jobs ,const MAXJOBNUM = 10000
size int // Size of jobs which jobs holding.
}
對於執行時間的控制,均通過對job的unit屬性進行設置,代碼如下
// Seconds set the unit with seconds
func (j *Job) Seconds() *Job {
return j.setUnit("seconds")
}
// Minutes set the unit with minute
func (j *Job) Minutes() *Job {
return j.setUnit("minutes")
}
// Second set the unit with second
func (j *Job) Second() *Job {
j.mustInterval(1)
return j.Seconds()
}
// Minute set the unit with minute, which interval is 1
func (j *Job) Minute() *Job {
j.mustInterval(1)
return j.Minutes()
}
// Sunday sets the job start day Sunday
func (j *Job) Sunday() *Job {
return j.Weekday(time.Sunday)
}
// Every schedule a new periodic job with interval
func (s *Scheduler) Every(interval uint64) *Job {
job := NewJob(interval)
s.jobs[s.size] = job
s.size++
return job
}
簡單總結
gocron代碼總共570行,在java中但凡涉及到一點“通用工具”或“架構”的實現,除了多依賴之外,擼代碼是少不了的。但在go中要實現滿足基本功能的cron任務調度,沒有多餘依賴,純基於gosdk本身,575行代碼打完收工。這是在不接觸go語言之前設想不到的事情。輕量好用,爲中間件而生。
編程語言跟人類溝通語言一樣,屬性都是工具,透過這個工具無論作爲人或是工程師,給我們打開的是另一個世界和景象。在對比中揚長避短,可對比的資源越多,越是能找到最優方案。不怕不知道,就怕不知道。