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)字段如果沒提供,相當于是 *
1
2
3
4
5
6
7
8
9
|
# ┌───────────── 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
2
3
4
5
6
7
8
9
10
11
|
constantdelay.go #一個最簡單的秒級別定時系統。與cron無關 constantdelay_test.go #測試 cron.go #Cron系統。管理一系列的cron定時任務(Schedule Job) cron_test.go #測試 doc.go #說明文檔 LICENSE #授權書 parser.go #解析器,解析cron格式字符串城一個具體的定時器(Schedule) parser_test.go #測試 README.md #README spec.go #單個定時器(Schedule)結構體。如何計算自己的下一次觸發時間 spec_test.go #測試 |
cron.go
結構體:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Cron keeps track of any number of entries, invoking the associated func as // specified by the schedule. It may be started, stopped, and the entries may // be inspected while running. // Cron保持任意數量的條目的軌道,調用相關的func時間表指定。它可以被啟動,停止和條目,可運行的同時進行檢查。 type Cron struct { entries []*Entry // 任務 stop chan struct{} // 叫停止的途徑 add chan *Entry // 添加新任務的方式 snapshot chan []*Entry // 請求獲取任務快照的方式 running bool // 是否在運行 ErrorLog *log.Logger // 出錯日志(新增屬性) location *time.Location // 所在地區(新增屬性) } |
1
2
3
4
5
6
7
8
9
10
11
12
|
// Entry consists of a schedule and the func to execute on that schedule. // 入口包括時間表和可在時間表上執行的func type Entry struct { // 計時器 Schedule Schedule // 下次執行時間 Next time.Time // 上次執行時間 Prev time.Time // 任務 Job Job } |
關鍵方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
// 開始任務 // Start the cron scheduler in its own go-routine, or no-op if already started. func (c *Cron) Start() { if c.running { return } c.running = true go c.run() } // 結束任務 // Stop stops the cron scheduler if it is running; otherwise it does nothing. func (c *Cron) Stop() { if !c.running { return } c.stop <- struct{}{} c.running = false } // 執行定時任務 // Run the scheduler.. this is private just due to the need to synchronize // access to the 'running' state variable. func (c *Cron) run() { // Figure out the next activation times for each entry. now := time.Now().In(c.location) for _, entry := range c.entries { entry.Next = entry.Schedule.Next(now) } // 無限循環 for { //通過對下一個執行時間進行排序,判斷那些任務是下一次被執行的,防在隊列的前面.sort是用來做排序的 sort.Sort(byTime(c.entries)) var effective time.Time if len(c.entries) == 0 || c.entries[0].Next.IsZero() { // If there are no entries yet, just sleep - it still handles new entries // and stop requests. effective = now.AddDate(10, 0, 0) } else { effective = c.entries[0].Next } timer := time.NewTimer(effective.Sub(now)) select { case now = <-timer.C: // 執行當前任務 now = now.In(c.location) // Run every entry whose next time was this effective time. for _, e := range c.entries { if e.Next != effective { break } go c.runWithRecovery(e.Job) e.Prev = e.Next e.Next = e.Schedule.Next(now) } continue case newEntry := <-c.add: // 添加新的任務 c.entries = append(c.entries, newEntry) newEntry.Next = newEntry.Schedule.Next(time.Now().In(c.location)) case <-c.snapshot: // 獲取快照 c.snapshot <- c.entrySnapshot() case <-c.stop: // 停止任務 timer.Stop() return } // 'now' should be updated after newEntry and snapshot cases. now = time.Now().In(c.location) timer.Stop() } } |
spec.go
結構體及關鍵方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
// SpecSchedule specifies a duty cycle (to the second granularity), based on a // traditional crontab specification. It is computed initially and stored as bit sets. type SpecSchedule struct { // 表達式中鎖表明的,秒,分,時,日,月,周,每個都是uint64 // Dom:Day of Month,Dow:Day of week Second, Minute, Hour, Dom, Month, Dow uint64 } // bounds provides a range of acceptable values (plus a map of name to value). // 定義了表達式的結構體 type bounds struct { min, max uint names map[string]uint } // The bounds for each field. // 這樣就能看出各個表達式的范圍 var ( seconds = bounds{0, 59, nil} minutes = bounds{0, 59, nil} hours = bounds{0, 23, nil} dom = bounds{1, 31, nil} months = bounds{1, 12, map[string]uint{ "jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12, }} dow = bounds{0, 6, map[string]uint{ "sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6, }} ) const ( // Set the top bit if a star was included in the expression. starBit = 1 << 63 ) |
看了上面的東西肯定有人疑惑為什么秒分時這些都是定義了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表達式的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/* ------------------------------------------------------------ 第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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
|
package cron import ( "fmt" "math" "strconv" "strings" "time" ) // Configuration options for creating a parser. Most options specify which // fields should be included, while others enable features. If a field is not // included the parser will assume a default value. These options do not change // the order fields are parse in. type ParseOption int const ( Second ParseOption = 1 << iota // Seconds field, default 0 Minute // Minutes field, default 0 Hour // Hours field, default 0 Dom // Day of month field, default * Month // Month field, default * Dow // Day of week field, default * DowOptional // Optional day of week field, default * Descriptor // Allow descriptors such as @monthly, @weekly, etc. ) var places = []ParseOption{ Second, Minute, Hour, Dom, Month, Dow, } var defaults = []string{ "0", "0", "0", "*", "*", "*", } // A custom Parser that can be configured. type Parser struct { options ParseOption optionals int } // Creates a custom Parser with custom options. // // // Standard parser without descriptors // specParser := NewParser(Minute | Hour | Dom | Month | Dow) // sched, err := specParser.Parse("0 0 15 */3 *") // // // Same as above, just excludes time fields // subsParser := NewParser(Dom | Month | Dow) // sched, err := specParser.Parse("15 */3 *") // // // Same as above, just makes Dow optional // subsParser := NewParser(Dom | Month | DowOptional) // sched, err := specParser.Parse("15 */3") // func NewParser(options ParseOption) Parser { optionals := 0 if options&DowOptional > 0 { options |= Dow optionals++ } return Parser{options, optionals} } // Parse returns a new crontab schedule representing the given spec. // It returns a descriptive error if the spec is not valid. // It accepts crontab specs and features configured by NewParser. // 將字符串解析成為SpecSchedule 。 SpecSchedule符合Schedule接口 func (p Parser) Parse(spec string) (Schedule, error) { // 直接處理特殊的特殊的字符串 if spec[0] == '@' && p.options&Descriptor > 0 { return parseDescriptor(spec) } // Figure out how many fields we need max := 0 for _, place := range places { if p.options&place > 0 { max++ } } min := max - p.optionals // cron利用空白拆解出獨立的items。 fields := strings.Fields(spec) // 驗證表達式取值范圍 if count := len(fields); count < min || count > max { if min == max { return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec) } return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec) } // Fill in missing fields fields = expandFields(fields, p.options) var err error field := func(field string, r bounds) uint64 { if err != nil { return 0 } var bits uint64 bits, err = getField(field, r) return bits } var ( second = field(fields[0], seconds) minute = field(fields[1], minutes) hour = field(fields[2], hours) dayofmonth = field(fields[3], dom) month = field(fields[4], months) dayofweek = field(fields[5], dow) ) if err != nil { return nil, err } // 返回所需要的SpecSchedule return &SpecSchedule{ Second: second, Minute: minute, Hour: hour, Dom: dayofmonth, Month: month, Dow: dayofweek, }, nil } func expandFields(fields []string, options ParseOption) []string { n := 0 count := len(fields) expFields := make([]string, len(places)) copy(expFields, defaults) for i, place := range places { if options&place > 0 { expFields[i] = fields[n] n++ } if n == count { break } } return expFields } var standardParser = NewParser( Minute | Hour | Dom | Month | Dow | Descriptor, ) // ParseStandard returns a new crontab schedule representing the given standardSpec // (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always // pass 5 entries representing: minute, hour, day of month, month and day of week, // in that order. It returns a descriptive error if the spec is not valid. // // It accepts // - Standard crontab specs, e.g. "* * * * ?" // - Descriptors, e.g. "@midnight", "@every 1h30m" // 這里表示不僅可以使用cron表達式,也可以使用@midnight @every等方法 func ParseStandard(standardSpec string) (Schedule, error) { return standardParser.Parse(standardSpec) } var defaultParser = NewParser( Second | Minute | Hour | Dom | Month | DowOptional | Descriptor, ) // Parse returns a new crontab schedule representing the given spec. // It returns a descriptive error if the spec is not valid. // // It accepts // - Full crontab specs, e.g. "* * * * * ?" // - Descriptors, e.g. "@midnight", "@every 1h30m" func Parse(spec string) (Schedule, error) { return defaultParser.Parse(spec) } // getField returns an Int with the bits set representing all of the times that // the field represents or error parsing field value. A "field" is a comma-separated // list of "ranges". func getField(field string, r bounds) (uint64, error) { var bits uint64 ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) for _, expr := range ranges { bit, err := getRange(expr, r) if err != nil { return bits, err } bits |= bit } return bits, nil } // getRange returns the bits indicated by the given expression: // number | number "-" number [ "/" number ] // or error parsing range. func getRange(expr string, r bounds) (uint64, error) { var ( start, end, step uint rangeAndStep = strings.Split(expr, "/") lowAndHigh = strings.Split(rangeAndStep[0], "-") singleDigit = len(lowAndHigh) == 1 err error ) var extra uint64 if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { start = r.min end = r.max extra = starBit } else { start, err = parseIntOrName(lowAndHigh[0], r.names) if err != nil { return 0, err } switch len(lowAndHigh) { case 1: end = start case 2: end, err = parseIntOrName(lowAndHigh[1], r.names) if err != nil { return 0, err } default: return 0, fmt.Errorf("Too many hyphens: %s", expr) } } switch len(rangeAndStep) { case 1: step = 1 case 2: step, err = mustParseInt(rangeAndStep[1]) if err != nil { return 0, err } // Special handling: "N/step" means "N-max/step". if singleDigit { end = r.max } default: return 0, fmt.Errorf("Too many slashes: %s", expr) } if start < r.min { return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) } if end > r.max { return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr) } if start > end { return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) } if step == 0 { return 0, fmt.Errorf("Step of range should be a positive number: %s", expr) } return getBits(start, end, step) | extra, nil } // parseIntOrName returns the (possibly-named) integer contained in expr. func parseIntOrName(expr string, names map[string]uint) (uint, error) { if names != nil { if namedInt, ok := names[strings.ToLower(expr)]; ok { return namedInt, nil } } return mustParseInt(expr) } // mustParseInt parses the given expression as an int or returns an error. func mustParseInt(expr string) (uint, error) { num, err := strconv.Atoi(expr) if err != nil { return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err) } if num < 0 { return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr) } return uint(num), nil } // getBits sets all bits in the range [min, max], modulo the given step size. func getBits(min, max, step uint) uint64 { var bits uint64 // If step is 1, use shifts. if step == 1 { return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) } // Else, use a simple loop. for i := min; i <= max; i += step { bits |= 1 << i } return bits } // all returns all bits within the given bounds. (plus the star bit) func all(r bounds) uint64 { return getBits(r.min, r.max, 1) | starBit } // parseDescriptor returns a predefined schedule for the expression, or error if none matches. func parseDescriptor(descriptor string) (Schedule, error) { switch descriptor { case "@yearly", "@annually": return &SpecSchedule{ Second: 1 << seconds.min, Minute: 1 << minutes.min, Hour: 1 << hours.min, Dom: 1 << dom.min, Month: 1 << months.min, Dow: all(dow), }, nil case "@monthly": return &SpecSchedule{ Second: 1 << seconds.min, Minute: 1 << minutes.min, Hour: 1 << hours.min, Dom: 1 << dom.min, Month: all(months), Dow: all(dow), }, nil case "@weekly": return &SpecSchedule{ Second: 1 << seconds.min, Minute: 1 << minutes.min, Hour: 1 << hours.min, Dom: all(dom), Month: all(months), Dow: 1 << dow.min, }, nil case "@daily", "@midnight": return &SpecSchedule{ Second: 1 << seconds.min, Minute: 1 << minutes.min, Hour: 1 << hours.min, Dom: all(dom), Month: all(months), Dow: all(dow), }, nil case "@hourly": return &SpecSchedule{ Second: 1 << seconds.min, Minute: 1 << minutes.min, Hour: all(hours), Dom: all(dom), Month: all(months), Dow: all(dow), }, nil } const every = "@every " if strings.HasPrefix(descriptor, every) { duration, err := time.ParseDuration(descriptor[len(every):]) if err != nil { return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err) } return Every(duration), nil } return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor) } |
項目中應用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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 這里面就不一一贅述了。希望大家能夠自己探索。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://www.cnblogs.com/zuxingyu/p/6023919.html