Go cron定时任务的用法

时间:2021-07-27 07:50:36

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