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