Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.11 Cron定時任務

專案地址:https://github.com/EDDYCJY/go-gin-example

知識點

  • 完成定時任務的功能

本文目標

在實際的應用專案中,定時任務的使用是很常見的。你是否有過 Golang 如何做定時任務的疑問,莫非是輪詢,在本文中我們將結合我們的專案講述 Cron。

介紹

我們將使用 cron 這個包,它實作了 cron 規範解析器和任務執行器,簡單來講就是包含了定時任務所需的功能

Cron 表示式格式

欄位名是否必填允許的值允許的特殊字元
秒(Seconds)Yes0-59* / , -
分(Minutes)Yes0-59* / , -
時(Hours)Yes0-23* / , -
一個月中的某天(Day of month)Yes1-31* / , - ?
月(Month)Yes1-12 or JAN-DEC* / , -
星期幾(Day of week)Yes0-6 or SUN-SAT* / , - ?

Cron表示式表示一組時間,使用 6 個空格分隔的欄位

可以留意到 Golang 的 Cron 比 Crontab 多了一個秒級,以後遇到秒級要求的時候就省事了

Cron 特殊字元

1、星號 ( * )

星號表示將匹配欄位的所有值

2、斜線 ( / )

斜線使用者 描述範圍的增量,表現為 “N-MAX/x”,first-last/x 的形式,例如 3-59/15 表示此時的第三分鐘和此後的每 15 分鐘,到59分鐘為止。即從 N 開始,使用增量直到該特定範圍結束。它不會重複

3、逗號 ( , )

逗號用於分隔列表中的專案。例如,在 Day of week 使用“MON,WED,FRI”將意味著星期一,星期三和星期五

4、連字元 ( - )

連字元用於定義範圍。例如,9 - 17 表示從上午 9 點到下午 5 點的每個小時

5、問號 ( ? )

不指定值,用於代替 “ * ”,類似 “ _ ” 的存在,不難理解

預定義的 Cron 時間表

輸入簡述相當於
@yearly (or @annually)1月1日午夜執行一次0 0 0 1 1 *
@monthly每個月的午夜,每個月的第一個月執行一次0 0 0 1
@weekly每週一次,週日午夜執行一次0 0 0 0
@daily (or @midnight)每天午夜執行一次0 0 0 *
@hourly每小時執行一次0 0

安裝

$ go get -u github.com/robfig/cron

實踐

在上一章節 Gin實踐 連載十 定製 GORM Callbacks 中,我們使用了 GORM 的回撥實作了軟刪除,同時也引入了另外一個問題

就是我怎麼硬刪除,我什麼時候硬刪除?這個往往與業務場景有關係,大致為

  • 另外有一套硬刪除介面
  • 定時任務清理(或轉移、backup)無效資料

在這裡我們選用第二種解決方案來進行實踐

編寫硬刪除程式碼

開啟 models 目錄下的 tag.go、article.go檔案,分別新增以下程式碼

1、tag.go

func CleanAllTag() bool {
    db.Unscoped().Where("deleted_on != ? ", 0).Delete(&Tag{})

    return true
}

2、article.go

func CleanAllArticle() bool {
    db.Unscoped().Where("deleted_on != ? ", 0).Delete(&Article{})

    return true
}

注意硬刪除要使用 Unscoped(),這是 GORM 的約定

編寫Cron

在 專案根目錄下新建 cron.go 檔案,用於編寫定時任務的程式碼,寫入檔案內容

package main

import (
    "time"
    "log"

    "github.com/robfig/cron"

    "github.com/EDDYCJY/go-gin-example/models"
)

func main() {
    log.Println("Starting...")

    c := cron.New()
    c.AddFunc("* * * * * *", func() {
        log.Println("Run models.CleanAllTag...")
        models.CleanAllTag()
    })
    c.AddFunc("* * * * * *", func() {
        log.Println("Run models.CleanAllArticle...")
        models.CleanAllArticle()
    })

    c.Start()

    t1 := time.NewTimer(time.Second * 10)
    for {
        select {
        case <-t1.C:
            t1.Reset(time.Second * 10)
        }
    }
}

在這段程式中,我們做了如下的事情

cron.New()

會根據本地時間建立一個新(空白)的 Cron job runner

func New() *Cron {
    return NewWithLocation(time.Now().Location())
}

// NewWithLocation returns a new Cron job runner.
func NewWithLocation(location *time.Location) *Cron {
    return &Cron{
        entries:  nil,
        add:      make(chan *Entry),
        stop:     make(chan struct{}),
        snapshot: make(chan []*Entry),
        running:  false,
        ErrorLog: nil,
        location: location,
    }
}

c.AddFunc()

AddFunc 會向 Cron job runner 新增一個 func ,以按給定的時間表執行

func (c *Cron) AddJob(spec string, cmd Job) error {
    schedule, err := Parse(spec)
    if err != nil {
        return err
    }
    c.Schedule(schedule, cmd)
    return nil
}

會首先解析時間表,如果填寫有問題會直接 err,無誤則將 func 新增到 Schedule 佇列中等待執行

func (c *Cron) Schedule(schedule Schedule, cmd Job) {
    entry := &Entry{
        Schedule: schedule,
        Job:      cmd,
    }
    if !c.running {
        c.entries = append(c.entries, entry)
        return
    }

    c.add <- entry
}

3、c.Start()

在當前執行的程式中啟動 Cron 排程程式。其實這裡的主體是 goroutine + for + select + timer 的排程控制哦

func (c *Cron) Run() {
    if c.running {
        return
    }
    c.running = true
    c.run()
}

time.NewTimer + for + select + t1.Reset

如果你是初學者,大概會有疑問,這是幹嘛用的?

(1)time.NewTimer

會建立一個新的定時器,持續你設定的時間 d 後傳送一個 channel 訊息

(2)for + select

阻塞 select 等待 channel

(3)t1.Reset

會重置定時器,讓它重新開始計時

注:本文適用於 “t.C已經取走,可直接使用 Reset”。

總的來說,這段程式是為了阻塞主程式而編寫的,希望你帶著疑問來想,有沒有別的辦法呢?

有的,你直接 select{} 也可以完成這個需求 :)

驗證

$ go run cron.go 
2018/04/29 17:03:34 [info] replacing callback `gorm:update_time_stamp` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:56
2018/04/29 17:03:34 [info] replacing callback `gorm:update_time_stamp` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:57
2018/04/29 17:03:34 [info] replacing callback `gorm:delete` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:58
2018/04/29 17:03:34 Starting...
2018/04/29 17:03:35 Run models.CleanAllArticle...
2018/04/29 17:03:35 Run models.CleanAllTag...
2018/04/29 17:03:36 Run models.CleanAllArticle...
2018/04/29 17:03:36 Run models.CleanAllTag...
2018/04/29 17:03:37 Run models.CleanAllTag...
2018/04/29 17:03:37 Run models.CleanAllArticle...

檢查輸出日誌正常,模擬已軟刪除的資料,定時任務工作OK

小結

定時任務很常見,希望你透過本文能夠熟知 Golang 怎麼實作一個簡單的定時任務排程管理

可以不依賴系統的 Crontab 設定,指不定哪一天就用上了呢

問題

如果你手動修改計算機的系統時間,是會導致定時任務錯亂的,所以一般不要亂來。

參考

本系列示例程式碼

關於

修改記錄

  • 第一版:2018年02月16日釋出文章
  • 第二版:2019年10月02日修改文章

如果有任何疑問或錯誤,歡迎在 issues 進行提問或給予修正意見,如果喜歡或對你有所幫助,歡迎 Star,對作者是一種鼓勵和推進。

我的微信公眾號

image