Go cron定時任務的用法


cron是什么

  cron的意思就是:計划任務,說白了就是定時任務。我和系統約個時間,你在幾點幾分幾秒或者每隔幾分鍾跑一個任務(job),就那么簡單。

cron表達式  

  cron表達式是一個好東西,這個東西不僅Java的quartZ能用到,Go語言中也可以用到。我沒有用過Linux的cron,但網上說Linux也是可以用crontab -e 命令來配置定時任務。Go語言和Java中都是可以精確到秒的,但是Linux中不行。

  cron表達式代表一個時間的集合,使用6個空格分隔的字段表示:

字段名 是否必須 允許的值  允許的特定字符
秒(Seconds) 0-59 * / , -
分(Minute) 0-59 * / , -
時(Hours) 0-23 * / , -
日(Day of month) 1-31 * / , - ?
月(Month) 1-12 或 JAN-DEC * / , -
星期(Day of week) 0-6 或 SUM-SAT * / , - ?

  

  

 

 

 

    注:

    1.月(Month)和星期(Day of week)字段的值不區分大小寫,如:SUN、Sun 和 sun 是一樣的。

    2.星期(Day of week)字段如果沒提供,相當於是 *

 # ┌───────────── min (0 - 59)
 # │ ┌────────────── hour (0 - 23)
 # │ │ ┌─────────────── day of month (1 - 31)
 # │ │ │ ┌──────────────── month (1 - 12)
 # │ │ │ │ ┌───────────────── day of week (0 - 6) (0 to 6 are Sunday to
 # │ │ │ │ │                  Saturday, or use names; 7 is also Sunday)
 # │ │ │ │ │
 # │ │ │ │ │
 # * * * * *  command to execute

cron特定字符說明

  1)星號(*)

    表示 cron 表達式能匹配該字段的所有值。如在第5個字段使用星號(month),表示每個月

  2)斜線(/)

    表示增長間隔,如第1個字段(minutes) 值是 3-59/15,表示每小時的第3分鍾開始執行一次,之后每隔 15 分鍾執行一次(即 3、18、33、48 這些時間點執行),這里也可以表示為:3/15

  3)逗號(,)

    用於枚舉值,如第6個字段值是 MON,WED,FRI,表示 星期一、三、五 執行

  4)連字號(-)

    表示一個范圍,如第3個字段的值為 9-17 表示 9am 到 5pm 直接每個小時(包括9和17)

  5)問號(?)

    只用於 日(Day of month) 和 星期(Day of week),表示不指定值,可以用於代替 *

  6)L,W,#

    Go中沒有L,W,#的用法,下文作解釋。

cron舉例說明

    每隔5秒執行一次:*/5 * * * * ?

            每隔1分鍾執行一次:0 */1 * * * ?

            每天23點執行一次:0 0 23 * * ?

            每天凌晨1點執行一次:0 0 1 * * ?

            每月1號凌晨1點執行一次:0 0 1 1 * ?

            在26分、29分、33分執行一次:0 26,29,33 * * * ?

            每天的0點、13點、18點、21點都執行一次:0 0 0,13,18,21 * * ?

下載安裝

  控制台輸入 go get github.com/robfig/cron 去下載定時任務的Go包,前提是你的 $GOPATH 已經配置好

源碼解析

  文件目錄講解 

 1 constantdelay.go      #一個最簡單的秒級別定時系統。與cron無關
 2 constantdelay_test.go #測試
 3 cron.go               #Cron系統。管理一系列的cron定時任務(Schedule Job)
 4 cron_test.go          #測試
 5 doc.go                #說明文檔
 6 LICENSE               #授權書 
 7 parser.go             #解析器,解析cron格式字符串城一個具體的定時器(Schedule)
 8 parser_test.go        #測試
 9 README.md             #README
10 spec.go               #單個定時器(Schedule)結構體。如何計算自己的下一次觸發時間
11 spec_test.go          #測試

  cron.go

    結構體:

 1 // Cron keeps track of any number of entries, invoking the associated func as
 2 // specified by the schedule. It may be started, stopped, and the entries may
 3 // be inspected while running. 
 4 // Cron保持任意數量的條目的軌道,調用相關的func時間表指定。它可以被啟動,停止和條目,可運行的同時進行檢查。
 5 type Cron struct {
 6     entries  []*Entry        // 任務
 7     stop     chan struct{}      // 叫停止的途徑
 8     add      chan *Entry        // 添加新任務的方式
 9     snapshot chan []*Entry      // 請求獲取任務快照的方式
10     running  bool               // 是否在運行
11     ErrorLog *log.Logger        // 出錯日志(新增屬性)
12     location *time.Location     // 所在地區(新增屬性)       
13 }

 

 1 // Entry consists of a schedule and the func to execute on that schedule.
 2 // 入口包括時間表和可在時間表上執行的func
 3 type Entry struct {
 4         // 計時器
 5     Schedule Schedule
 6     // 下次執行時間
 7     Next time.Time
 8     // 上次執行時間
 9     Prev time.Time
10     // 任務
11     Job Job
12 }

 

    關鍵方法:

 1 //  開始任務
 2 // Start the cron scheduler in its own go-routine, or no-op if already started.
 3 func (c *Cron) Start() {
 4     if c.running {
 5         return
 6     }
 7     c.running = true
 8     go c.run()
 9 }
10 // 結束任務
11 // Stop stops the cron scheduler if it is running; otherwise it does nothing.
12 func (c *Cron) Stop() {
13     if !c.running {
14         return
15     }
16     c.stop <- struct{}{}
17     c.running = false
18 }
19 
20 // 執行定時任務
21 // Run the scheduler.. this is private just due to the need to synchronize
22 // access to the 'running' state variable.
23 func (c *Cron) run() {
24     // Figure out the next activation times for each entry.
25     now := time.Now().In(c.location)
26     for _, entry := range c.entries {
27         entry.Next = entry.Schedule.Next(now)
28     }
29         // 無限循環
30     for {
31             //通過對下一個執行時間進行排序,判斷那些任務是下一次被執行的,防在隊列的前面.sort是用來做排序的
32         sort.Sort(byTime(c.entries))
33 
34         var effective time.Time
35         if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
36             // If there are no entries yet, just sleep - it still handles new entries
37             // and stop requests.
38             effective = now.AddDate(10, 0, 0)
39         } else {
40             effective = c.entries[0].Next
41         }
42 
43         timer := time.NewTimer(effective.Sub(now))
44         select {
45         case now = <-timer.C:  // 執行當前任務
46             now = now.In(c.location)
47             // Run every entry whose next time was this effective time.
48             for _, e := range c.entries {
49                 if e.Next != effective {
50                     break
51                 }
52                 go c.runWithRecovery(e.Job)
53                 e.Prev = e.Next
54                 e.Next = e.Schedule.Next(now)
55             }
56             continue
57 
58         case newEntry := <-c.add:  // 添加新的任務
59             c.entries = append(c.entries, newEntry)
60             newEntry.Next = newEntry.Schedule.Next(time.Now().In(c.location))
61 
62         case <-c.snapshot:  // 獲取快照
63             c.snapshot <- c.entrySnapshot()
64 
65         case <-c.stop:   // 停止任務
66             timer.Stop()
67             return
68         }
69 
70         // 'now' should be updated after newEntry and snapshot cases.
71         now = time.Now().In(c.location)
72         timer.Stop()
73     }
74 }

 

      spec.go

  結構體及關鍵方法:

 1 // SpecSchedule specifies a duty cycle (to the second granularity), based on a
 2 // traditional crontab specification. It is computed initially and stored as bit sets.
 3 type SpecSchedule struct {
 4     // 表達式中鎖表明的,秒,分,時,日,月,周,每個都是uint64
 5     // Dom:Day of Month,Dow:Day of week
 6     Second, Minute, Hour, Dom, Month, Dow uint64
 7 }
 8 
 9 // bounds provides a range of acceptable values (plus a map of name to value).
10 // 定義了表達式的結構體
11 type bounds struct {
12     min, max uint
13     names    map[string]uint
14 }
15 
16 
17 // The bounds for each field.
18 // 這樣就能看出各個表達式的范圍
19 var (
20        seconds = bounds{0, 59, nil}
21        minutes = bounds{0, 59, nil}
22        hours   = bounds{0, 23, nil}
23        dom     = bounds{1, 31, nil}
24        months  = bounds{1, 12, map[string]uint{
25               "jan": 1,
26               "feb": 2,
27               "mar": 3,
28               "apr": 4,
29               "may": 5,
30               "jun": 6,
31               "jul": 7,
32               "aug": 8,
33               "sep": 9,
34               "oct": 10,
35               "nov": 11,
36               "dec": 12,
37        }}
38        dow = bounds{0, 6, map[string]uint{
39               "sun": 0,
40               "mon": 1,
41               "tue": 2,
42               "wed": 3,
43               "thu": 4,
44               "fri": 5,
45               "sat": 6,
46        }}
47 )
48 
49 const (
50        // Set the top bit if a star was included in the expression.
51        starBit = 1 << 63
52 )

 

  看了上面的東西肯定有人疑惑為什么秒分時這些都是定義了unit64,以及定義了一個常量starBit = 1 << 63這種寫法,這是邏輯運算符。表示二進制1向左移動63位。原因如下:

cron表達式是用來表示一系列時間的,而時間是無法逃脫自己的區間的 , 分,秒 0 - 59 , 時 0 - 23 , 天/月 0 - 31 , 天/周 0 - 6 , 月0 - 11 。 這些本質上都是一個點集合,或者說是一個整數區間。 那么對於任意的整數區間 , 可以描述cron的如下部分規則。

  • * | ? 任意 , 對應區間上的所有點。 ( 額外注意 日/周 , 日 / 月 的相互干擾。)
  • 純數字 , 對應一個具體的點。
  • / 分割的兩個數字 a , b, 區間上符合 a + n * b 的所有點 ( n >= 0 )。
  • - 分割的兩個數字, 對應這兩個數字決定的區間內的所有點。
  • L | W 需要對於特定的時間特殊判斷, 無法通用的對應到區間上的點。

 

至此, robfig/cron為什么不支持 L | W的原因已經明了了。去除這兩條規則后, 其余的規則其實完全可以使用點的窮舉來通用表示。 考慮到最大的區間也不過是60個點,那么使用一個uint64的整數的每一位來表示一個點便很合適了。所以定義unit64不為過

下面是go中cron表達式的方法:

/* 
   ------------------------------------------------------------
   第64位標記任意 , 用於 日/周 , 日 / 月 的相互干擾。
   63 - 0 為 表示區間 [63 , 0] 的 每一個點。
   ------------------------------------------------------------ 

   假設區間是 0 - 63 , 則有如下的例子 : 

   比如  0/3 的表示如下 : (表示每隔兩位為1)
   * / ?       
   +---+--------------------------------------------------------+
   | 0 | 1 0 0 1 0 0 1  ~~  ~~                    1 0 0 1 0 0 1 |
   +---+--------------------------------------------------------+   
        63 ~ ~                                           ~~ 0 

   比如  2-5 的表示如下 : (表示從右往左2-5位上都是1)
   * / ?       
   +---+--------------------------------------------------------+
   | 0 | 0 0 0 0 ~  ~      ~~            ~    0 0 0 1 1 1 1 0 0 |
   +---+--------------------------------------------------------+   
        63 ~ ~                                           ~~ 0 

  比如  * 的表示如下 : (表示所有位置上都為1)
   * / ?       
   +---+--------------------------------------------------------+
   | 1 | 1 1 1 1 1 ~  ~                  ~    1 1 1 1 1 1 1 1 1 |
   +---+--------------------------------------------------------+   
        63 ~ ~                                           ~~ 0 
*/

  parser.go

  將字符串解析為SpecSchedule的類。

 

  

  1 package cron
  2 
  3 import (
  4     "fmt"
  5     "math"
  6     "strconv"
  7     "strings"
  8     "time"
  9 )
 10 
 11 // Configuration options for creating a parser. Most options specify which
 12 // fields should be included, while others enable features. If a field is not
 13 // included the parser will assume a default value. These options do not change
 14 // the order fields are parse in.
 15 type ParseOption int
 16 
 17 const (
 18     Second      ParseOption = 1 << iota // Seconds field, default 0
 19     Minute                              // Minutes field, default 0
 20     Hour                                // Hours field, default 0
 21     Dom                                 // Day of month field, default *
 22     Month                               // Month field, default *
 23     Dow                                 // Day of week field, default *
 24     DowOptional                         // Optional day of week field, default *
 25     Descriptor                          // Allow descriptors such as @monthly, @weekly, etc.
 26 )
 27 
 28 var places = []ParseOption{
 29     Second,
 30     Minute,
 31     Hour,
 32     Dom,
 33     Month,
 34     Dow,
 35 }
 36 
 37 var defaults = []string{
 38     "0",
 39     "0",
 40     "0",
 41     "*",
 42     "*",
 43     "*",
 44 }
 45 
 46 // A custom Parser that can be configured.
 47 type Parser struct {
 48     options   ParseOption
 49     optionals int
 50 }
 51 
 52 // Creates a custom Parser with custom options.
 53 //
 54 //  // Standard parser without descriptors
 55 //  specParser := NewParser(Minute | Hour | Dom | Month | Dow)
 56 //  sched, err := specParser.Parse("0 0 15 */3 *")
 57 //
 58 //  // Same as above, just excludes time fields
 59 //  subsParser := NewParser(Dom | Month | Dow)
 60 //  sched, err := specParser.Parse("15 */3 *")
 61 //
 62 //  // Same as above, just makes Dow optional
 63 //  subsParser := NewParser(Dom | Month | DowOptional)
 64 //  sched, err := specParser.Parse("15 */3")
 65 //
 66 func NewParser(options ParseOption) Parser {
 67     optionals := 0
 68     if options&DowOptional > 0 {
 69         options |= Dow
 70         optionals++
 71     }
 72     return Parser{options, optionals}
 73 }
 74 
 75 // Parse returns a new crontab schedule representing the given spec.
 76 // It returns a descriptive error if the spec is not valid.
 77 // It accepts crontab specs and features configured by NewParser.
 78 // 將字符串解析成為SpecSchedule 。 SpecSchedule符合Schedule接口
 79 
 80 func (p Parser) Parse(spec string) (Schedule, error) {
 81   // 直接處理特殊的特殊的字符串
 82     if spec[0] == '@' && p.options&Descriptor > 0 {
 83         return parseDescriptor(spec)
 84     }
 85 
 86     // Figure out how many fields we need
 87     max := 0
 88     for _, place := range places {
 89         if p.options&place > 0 {
 90             max++
 91         }
 92     }
 93     min := max - p.optionals
 94 
 95     // cron利用空白拆解出獨立的items。
 96     fields := strings.Fields(spec)
 97 
 98     // 驗證表達式取值范圍
 99     if count := len(fields); count < min || count > max {
100         if min == max {
101             return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)
102         }
103         return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)
104     }
105 
106     // Fill in missing fields
107     fields = expandFields(fields, p.options)
108 
109     var err error
110     field := func(field string, r bounds) uint64 {
111         if err != nil {
112             return 0
113         }
114         var bits uint64
115         bits, err = getField(field, r)
116         return bits
117     }
118 
119     var (
120         second     = field(fields[0], seconds)
121         minute     = field(fields[1], minutes)
122         hour       = field(fields[2], hours)
123         dayofmonth = field(fields[3], dom)
124         month      = field(fields[4], months)
125         dayofweek  = field(fields[5], dow)
126     )
127     if err != nil {
128         return nil, err
129     }
130     // 返回所需要的SpecSchedule
131     return &SpecSchedule{
132         Second: second,
133         Minute: minute,
134         Hour:   hour,
135         Dom:    dayofmonth,
136         Month:  month,
137         Dow:    dayofweek,
138     }, nil
139 }
140 
141 func expandFields(fields []string, options ParseOption) []string {
142     n := 0
143     count := len(fields)
144     expFields := make([]string, len(places))
145     copy(expFields, defaults)
146     for i, place := range places {
147         if options&place > 0 {
148             expFields[i] = fields[n]
149             n++
150         }
151         if n == count {
152             break
153         }
154     }
155     return expFields
156 }
157 
158 var standardParser = NewParser(
159     Minute | Hour | Dom | Month | Dow | Descriptor,
160 )
161 
162 // ParseStandard returns a new crontab schedule representing the given standardSpec
163 // (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
164 // pass 5 entries representing: minute, hour, day of month, month and day of week,
165 // in that order. It returns a descriptive error if the spec is not valid.
166 //
167 // It accepts
168 //   - Standard crontab specs, e.g. "* * * * ?"
169 //   - Descriptors, e.g. "@midnight", "@every 1h30m"
170 // 這里表示不僅可以使用cron表達式,也可以使用@midnight @every等方法
171 
172 func ParseStandard(standardSpec string) (Schedule, error) {
173     return standardParser.Parse(standardSpec)
174 }
175 
176 var defaultParser = NewParser(
177     Second | Minute | Hour | Dom | Month | DowOptional | Descriptor,
178 )
179 
180 // Parse returns a new crontab schedule representing the given spec.
181 // It returns a descriptive error if the spec is not valid.
182 //
183 // It accepts
184 //   - Full crontab specs, e.g. "* * * * * ?"
185 //   - Descriptors, e.g. "@midnight", "@every 1h30m"
186 func Parse(spec string) (Schedule, error) {
187     return defaultParser.Parse(spec)
188 }
189 
190 // getField returns an Int with the bits set representing all of the times that
191 // the field represents or error parsing field value.  A "field" is a comma-separated
192 // list of "ranges".
193 func getField(field string, r bounds) (uint64, error) {
194     var bits uint64
195     ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
196     for _, expr := range ranges {
197         bit, err := getRange(expr, r)
198         if err != nil {
199             return bits, err
200         }
201         bits |= bit
202     }
203     return bits, nil
204 }
205 
206 // getRange returns the bits indicated by the given expression:
207 //   number | number "-" number [ "/" number ]
208 // or error parsing range.
209 func getRange(expr string, r bounds) (uint64, error) {
210     var (
211         start, end, step uint
212         rangeAndStep     = strings.Split(expr, "/")
213         lowAndHigh       = strings.Split(rangeAndStep[0], "-")
214         singleDigit      = len(lowAndHigh) == 1
215         err              error
216     )
217 
218     var extra uint64
219     if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
220         start = r.min
221         end = r.max
222         extra = starBit
223     } else {
224         start, err = parseIntOrName(lowAndHigh[0], r.names)
225         if err != nil {
226             return 0, err
227         }
228         switch len(lowAndHigh) {
229         case 1:
230             end = start
231         case 2:
232             end, err = parseIntOrName(lowAndHigh[1], r.names)
233             if err != nil {
234                 return 0, err
235             }
236         default:
237             return 0, fmt.Errorf("Too many hyphens: %s", expr)
238         }
239     }
240 
241     switch len(rangeAndStep) {
242     case 1:
243         step = 1
244     case 2:
245         step, err = mustParseInt(rangeAndStep[1])
246         if err != nil {
247             return 0, err
248         }
249 
250         // Special handling: "N/step" means "N-max/step".
251         if singleDigit {
252             end = r.max
253         }
254     default:
255         return 0, fmt.Errorf("Too many slashes: %s", expr)
256     }
257 
258     if start < r.min {
259         return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
260     }
261     if end > r.max {
262         return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
263     }
264     if start > end {
265         return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
266     }
267     if step == 0 {
268         return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)
269     }
270 
271     return getBits(start, end, step) | extra, nil
272 }
273 
274 // parseIntOrName returns the (possibly-named) integer contained in expr.
275 func parseIntOrName(expr string, names map[string]uint) (uint, error) {
276     if names != nil {
277         if namedInt, ok := names[strings.ToLower(expr)]; ok {
278             return namedInt, nil
279         }
280     }
281     return mustParseInt(expr)
282 }
283 
284 // mustParseInt parses the given expression as an int or returns an error.
285 func mustParseInt(expr string) (uint, error) {
286     num, err := strconv.Atoi(expr)
287     if err != nil {
288         return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
289     }
290     if num < 0 {
291         return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
292     }
293 
294     return uint(num), nil
295 }
296 
297 // getBits sets all bits in the range [min, max], modulo the given step size.
298 func getBits(min, max, step uint) uint64 {
299     var bits uint64
300 
301     // If step is 1, use shifts.
302     if step == 1 {
303         return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
304     }
305 
306     // Else, use a simple loop.
307     for i := min; i <= max; i += step {
308         bits |= 1 << i
309     }
310     return bits
311 }
312 
313 // all returns all bits within the given bounds.  (plus the star bit)
314 func all(r bounds) uint64 {
315     return getBits(r.min, r.max, 1) | starBit
316 }
317 
318 // parseDescriptor returns a predefined schedule for the expression, or error if none matches.
319 func parseDescriptor(descriptor string) (Schedule, error) {
320     switch descriptor {
321     case "@yearly", "@annually":
322         return &SpecSchedule{
323             Second: 1 << seconds.min,
324             Minute: 1 << minutes.min,
325             Hour:   1 << hours.min,
326             Dom:    1 << dom.min,
327             Month:  1 << months.min,
328             Dow:    all(dow),
329         }, nil
330 
331     case "@monthly":
332         return &SpecSchedule{
333             Second: 1 << seconds.min,
334             Minute: 1 << minutes.min,
335             Hour:   1 << hours.min,
336             Dom:    1 << dom.min,
337             Month:  all(months),
338             Dow:    all(dow),
339         }, nil
340 
341     case "@weekly":
342         return &SpecSchedule{
343             Second: 1 << seconds.min,
344             Minute: 1 << minutes.min,
345             Hour:   1 << hours.min,
346             Dom:    all(dom),
347             Month:  all(months),
348             Dow:    1 << dow.min,
349         }, nil
350 
351     case "@daily", "@midnight":
352         return &SpecSchedule{
353             Second: 1 << seconds.min,
354             Minute: 1 << minutes.min,
355             Hour:   1 << hours.min,
356             Dom:    all(dom),
357             Month:  all(months),
358             Dow:    all(dow),
359         }, nil
360 
361     case "@hourly":
362         return &SpecSchedule{
363             Second: 1 << seconds.min,
364             Minute: 1 << minutes.min,
365             Hour:   all(hours),
366             Dom:    all(dom),
367             Month:  all(months),
368             Dow:    all(dow),
369         }, nil
370     }
371 
372     const every = "@every "
373     if strings.HasPrefix(descriptor, every) {
374         duration, err := time.ParseDuration(descriptor[len(every):])
375         if err != nil {
376             return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
377         }
378         return Every(duration), nil
379     }
380 
381     return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
382 }

 

項目中應用

   

package main

import (
    "github.com/robfig/cron"
    "log"
)

func main() {
    i := 0
    c := cron.New()
    spec := "*/5 * * * * ?"
    c.AddFunc(spec, func() {
        i++
        log.Println("cron running:", i)
    })
    c.AddFunc("@every 1h1m", func() {
        i++
        log.Println("cron running:", i)
    })
    c.Start()
}

  注: @every 用法比較特殊,這是Go里面比較特色的用法。同樣的還有 @yearly @annually @monthly @weekly @daily @midnight @hourly 這里面就不一一贅述了。希望大家能夠自己探索。

  

參考網站:

http://blog.studygolang.com/2014/02/go_crontab/

http://blog.csdn.net/cchd0001/article/details/51076922

https://en.wikipedia.org/wiki/Cron

https://github.com/robfig/cron


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM