Go定时器cron的使用详解

作者:骑头猪逛街 时间:2024-04-28 10:47:43 

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 已经配置好

源码解析

文件目录讲解


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

结构体:


// 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   // 所在地区(新增属性)    
}

// 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
}

关键方法:


// 开始任务
// 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

结构体及关键方法:


// 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的如下部分规则。

  1. * | ? 任意 , 对应区间上的所有点。 ( 额外注意 日/周 , 日 / 月 的相互干扰。)

  2. 纯数字 , 对应一个具体的点。

  3. / 分割的两个数字 a , b, 区间上符合 a + n * b 的所有点 ( n >= 0 )。

  4. - 分割的两个数字, 对应这两个数字决定的区间内的所有点。

  5. 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的类。


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)
}

项目中应用


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

标签:Go,定时器,cron
0
投稿

猜你喜欢

  • python 如何对logging日志封装

    2023-07-06 11:27:36
  • python关于变量名的基础知识点

    2023-07-27 14:21:43
  • python读取视频流提取视频帧的两种方法

    2023-04-04 15:10:22
  • java使用正则表达式判断邮箱格式是否正确的方法

    2022-07-24 20:31:10
  • django之自定义软删除Model的方法

    2023-11-17 21:31:12
  • 跟老齐学Python之通过Python连接数据库

    2024-01-13 09:12:30
  • Python通过wordcloud库实现将单词生成词云

    2022-02-24 20:17:18
  • Python通过TensorFLow进行线性模型训练原理与实现方法详解

    2022-11-10 16:17:27
  • PHP中Too few arguments to function的问题及解决

    2023-06-04 21:15:55
  • Python+selenium实现自动循环扔QQ邮箱漂流瓶

    2021-07-12 23:46:28
  • 一文带你学会MySQL的select语句

    2024-01-16 01:07:30
  • python发送邮件实例分享

    2021-07-17 23:14:23
  • 关于文件命名

    2009-09-18 16:29:00
  • Python找出列表中出现次数最多的元素三种方式

    2021-04-11 11:45:40
  • 5个款MySQL常用维护管理工具

    2010-10-25 19:45:00
  • SQL Server Parameter Sniffing及其改进方法

    2024-01-28 02:56:30
  • opencv 图像加法与图像融合的实现代码

    2021-07-20 14:57:30
  • python使用timeit时间模块

    2023-03-05 16:27:37
  • windows下Mysql多实例部署的操作方法

    2024-01-19 16:33:11
  • python3的pip路径在哪

    2023-01-27 14:15:39
  • asp之家 网络编程 m.aspxhome.com