golang自定义cron任务

原文链接: http://chuquanl.com/golang-cron%E7%AE%80%E4%BB%8B%E5%8F%8A%E4%BD%BFcron%E6%94%AF%E6%8C%81%E5%B8%A6%E5%8F%82%E6%95%B0%E4%BB%BB%E5%8A%A1%E8%B0%83%E7%94%A8/

需要注意一点的cron.v3(gopkg.in/robfig/cron.v3)和cron(github.com/robfig/cron)的使用完全不同,linux风格的表达式:0 12 * * *,表示每天12点执行一次,在cron里会被补全成0 0 12 * * *,而在cron.v3里会被补全成0 12 * * * *,相当于向左挪了一位,把『时』挪到了『分』,会出问题,这个坑我踩过。

 

下面文章转自:

感谢原作者。

 

老大让我写个cron的笔记,好像好久没有写过东西了。。。

先做一下搬运工

cron的使用

设置好gopath,使用命令
go get github.com/robfig/cron获取包支持

源码地址 https://github.com/robfig/cron

使用示例

 

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.Start()

 

    select{}

}

 

 

cron 表达式的基本格式

字段名 是否必须 允许的值 允许的特定字符
秒(Seconds) 0-59 * / , –
分(Minutes) 0-59 * / , –
时(Hours) 0-23 * / , –
日(Day of month) 1-31 * / , – ?
月(Month) 1-12 or JAN-DEC * / , –
星期(Day of week) 0-6 or SUM-SAT * / , – ?

特殊符号说明

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),表示不指定值,可以用于代替 *

主要类型和接口说明

1.cron:包含一系列要执行的实体;支持暂停【stop】;添加实体等

 

1

2

3

4

5

6

7

8

type Cron struct {

    entries  []*Entry

    stop     chan struct{}   // 控制 Cron 实例暂停

    add      chan *Entry     // 当 Cron 已经运行了,增加新的 Entity 是通过 add 这个 channel 实现的

    snapshot chan []*Entry   // 获取当前所有 entity 的快照

    running  bool            // 当已经运行时为true;否则为false

}

 

 

注意,Cron 结构没有导出任何成员。
注意:有一个成员 stop,类型是 struct{},即空结构体。

2.Entry:调度实体

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

type Entry struct {

    // The schedule on which this job should be run.

    // 负责调度当前 Entity 中的 Job 执行

    Schedule Schedule

 

    // The next time the job will run. This is the zero time if Cron has not been

    // started or this entry's schedule is unsatisfiable

    // Job 下一次执行的时间

    Next time.Time

 

    // The last time this job was run. This is the zero time if the job has never

    // been run.

    // 上一次执行时间

    Prev time.Time

 

    // The Job to run.

    // 要执行的 Job

    Job Job

}

 

 

3.Job:每一个实体包含一个需要运行的Job

这是一个接口,只有一个方法:run

 

1

2

3

4

type Job interface {

    Run()

}

 

 

由于 Entity 中需要 Job 类型,因此,我们希望定期运行的任务,就需要实现 Job 接口。同时,由于
Job接口只有一个无参数无返回值的方法,为了使用方便,作者提供了一个类型:

 

1

2

3

type FuncJob func()

func (f FuncJob) Run() { f() }

 

 

它通过简单的实现 Run() 方法来实现 Job 接口,这样,任何无参数无返回值的函数,通过强制类型转换为 FuncJob,就可以当作 Job 来使用了,AddFunc 方法 就是这么做的。所以需要修改带参数功能的job时从此处下手

Schedule:每个实体包含一个调度器(Schedule)

负责调度 Job 的执行。它也是一个接口。

 

1

2

3

4

5

6

7

type Schedule interface {

    // Return the next activation time, later than the given time.

    // Next is invoked initially, and then each time the job is run.

    // 返回同一 Entity 中的 Job 下一次执行的时间

    Next(time.Time) time.Time

}

 

 

Schedule 的具体实现通过解析 Cron 表达式得到。
库中提供了 Schedule 的两个具体实现,分别是 SpecSchedule 和 ConstantDelaySchedule。

  • SpecSchedule

 

1

2

3

4

type SpecSchedule struct {

    Second, Minute, Hour, Dom, Month, Dow uint64

}

 

 

  • ConstantDelaySchedule

 

1

2

3

4

type ConstantDelaySchedule struct {

    Delay time.Duration // 循环的时间间隔

}

 

 

这是一个简单的循环调度器,如:每 5 分钟。注意,最小单位是秒,不能比秒还小,比如 毫秒。
通过 Every 函数可以获取该类型的实例,如:

 

1

2

constDelaySchedule := Every(5e9)

 

 

得到的是一个每 5 秒执行一次的调度器。

4、主要实例化方法

1. 函数

  1. 实例化 Cron

 

1

2

3

4

5

6

7

8

9

10

func New() *Cron {

    return &Cron{

        entries:  nil,

        add:      make(chan *Entry),

        stop:     make(chan struct{}),

        snapshot: make(chan []*Entry),

        running:  false,

    }

}

 

 

可见实例化时,成员使用的基本是默认值;

  1. 解析 Cron 表达式

 

1

2

func Parse(spec string) (_ Schedule, err error)

 

 

spec 可以是:
* Full crontab specs, e.g. “* * * * * ?”
* Descriptors, e.g. “@midnight”, “@every 1h30m”

2.成员方法

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

// 将 job 加入 Cron 中

// 如上所述,该方法只是简单的通过 FuncJob 类型强制转换 cmd,然后调用 AddJob 方法

func (c *Cron) AddFunc(spec string, cmd func()) error

 

// 将 job 加入 Cron 中

// 通过 Parse 函数解析 cron 表达式 spec 的到调度器实例(Schedule),之后调用 c.Schedule 方法

func (c *Cron) AddJob(spec string, cmd Job) error

 

// 获取当前 Cron 总所有 Entities 的快照

func (c *Cron) Entries() []*Entry

 

// 通过两个参数实例化一个 Entity,然后加入当前 Cron 中

// 注意:如果当前 Cron 未运行,则直接将该 entity 加入 Cron 中;

// 否则,通过 add 这个成员 channel 将 entity 加入正在运行的 Cron 中

func (c *Cron) Schedule(schedule Schedule, cmd Job)

 

// 新启动一个 goroutine 运行当前 Cron

func (c *Cron) Start()

 

// 通过给 stop 成员发送一个 struct{}{} 来停止当前 Cron,同时将 running 置为 false

// 从这里知道,stop 只是通知 Cron 停止,因此往 channel 发一个值即可,而不关心值是多少

// 所以,成员 stop 定义为空 struct

func (c *Cron) Stop()

 

 

这几个函数就是文章开头的example中使用的函数,

从AddFunc函数说起 带参数任务的实现

 

1

2

3

4

func (c *Cron) AddFunc(spec string, cmd func()) error {

    return c.AddJob(spec, FuncJob(cmd))

}

 

 

AddFunc 含有两个参数,第一个是 cron表达式,这个不解释,第二个是func()类型参数cmd 即无参数无返回类型函数,下一步中直接将此参数强制转换为FuncJob类型,并调用AddJob函数

  • FuncJob类型

 

1

2

3

4

5

// A wrapper that turns a func() into a cron.Job

type FuncJob func()

 

func (f FuncJob) Run() { f() }

 

 

由上述代码可知FuncJob为自定义类型,真实类型为 func(),此类型实现了一个Run()方法

  • AddJob 函数

 

1

2

3

4

5

6

7

8

9

10

// AddJob adds a Job to the Cron to be run on the given schedule.

func (c *Cron) AddJob(spec string, cmd Job) error {

    schedule, err := Parse(spec)

    if err != nil {

        return err

    }

    c.Schedule(schedule, cmd)

    return nil

}

 

 

首先 AddJob 函数的传入参数为一个string类型的cron表达式和一个Job类型的cmd参数,但在AddFunc函数中,我们传入的第二个参数为FuncJob类型,所以Job类型应该是一个接口,在解析了cron表达式无错误以后,调用Schedule方法将cmd添加进了调度器

  • Job 类型

 

1

2

3

4

5

// Job is an interface for submitted cron jobs.

type Job interface {

    Run()

}

 

 

由此可知,Job是带有一个Run方法的接口类型,经过代码分析可以指定,cron定时调度时间到达时,将调用此
方法,也就是意味着,任何实现了Run方法的实例,都可以作为AddJob函数的cmd参数,而Run方法所实现的内容
就是你定时调度所需执行的任务(AddFunc函数只能添加无参数无返回的任务,太鸡肋了),接下来我们就来实现一
个带参数的任务添加

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

30

31

32

33

34

35

36

37

38

//定义一个类型 包含一个int类型参数和函数体

type funcIntJob struct {

    num   int

    function func(int)

}

 

//实现这个类型的Run()方法 使得可以传入Job接口

func (this *funcIntJob) Run() {

    if nil != this.function {

        this.function(this.num)

    }

}

 

//非必须  返回一个urlServeJob指针

func newfuncIntJob(num int, function funcInt) *urlServeJob {

    instance := &funcIntJob{

        num:   num,

        function: function,

    }

    return instance

}

 

//示例任务

func shownum(num int){

  fmt.Println(num)

}

 

func main(){

  var c = cron.New()

  job := newfuncIntJob(3, shownum)

  spec := "*/5 * * * * ?"

    c.AddJob(spec, job)

  c.Start()

  defer c.Stop()

  select{}

}

 

 

 

调度分析

我们就以AddFunc函数的之后开始分析cron的调度

任务添加

前面说到,AddFunc函数调用了AddJob函数,这里再看一次AddJob函数

 

1

2

3

4

5

6

7

8

9

10

// AddJob adds a Job to the Cron to be run on the given schedule.

func (c *Cron) AddJob(spec string, cmd Job) error {

    schedule, err := Parse(spec)

    if err != nil {

        return err

    }

    c.Schedule(schedule, cmd)

    return nil

}

 

 

这里有两个值得注意的东东,cron表达式解析后的schedule变量,以及Cron的Schedule方法
前面搬砖的时候有说到Schedule类型也是一个接口,再看一眼

 

1

2

3

4

5

6

type Schedule interface {

    // Return the next activation time, later than the given time.

    // Next is invoked initially, and then each time the job is run.

    Next(time.Time) time.Time

}

 

 

接口只有一个方法Next,参数返回都是Time,看样子应该是传入时间返回下次执行时间的一个方法,不过,
有时候乱猜真的会害死人

总之,Parse函数在经过一系列复杂的解析之后,返回了一个SpecSchedule类型变量,就是长这样的

 

1

2

3

4

5

6

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

    Second, Minute, Hour, Dom, Month, Dow uint64

}

 

 

这种类型含有一个Next方法,代码太复杂就不贴了,方法里面的东东我也不太懂,应该是,算了,不应该了(应该
输入参数加上是SpecSchedule的间隔后返回吧)

接下来看Cron类型的Schedule方法

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

// Schedule adds a Job to the Cron to be run on the given schedule.

func (c *Cron) Schedule(schedule Schedule, cmd Job) {

    entry := &Entry{

        Schedule: schedule,

        Job:      cmd,

    }

    if !c.running {

        c.entries = append(c.entries, entry)

        return

    }

 

    c.add <- entry

}

 

 

这个比较好理解,根据schedule和cmd参数构建了一个Entry变量,并且将这个变量添加进Cron的entries中
只不过在没有运行的时候直接添加,运行的时候通过chan添加,到这里就添加部分就告一段落了

调度

调度的开始实施是从Cron.Start()函数开始的

 

1

2

3

4

5

6

7

8

9

// 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()

}

 

 

东西很少,就是开了一个routine执行任务,这里cron还提供了一个使用当前routine执行的方法Run(),

 

1

2

3

4

5

6

7

8

9

// Run the cron scheduler, or no-op if already running.

func (c *Cron) Run() {

    if c.running {

        return

    }

    c.running = true

    c.run()

}

 

 

先不管这些,接下来重点到run()方法

 

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

// 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 := c.now()  //以当前时区的方式获取当前时间

    for _, entry := range c.entries {  

        entry.Next = entry.Schedule.Next(now) //得到entries中的每一个entry更新下一次执行时间

    }

 

    for {

        // Determine the next entry to run.

        sort.Sort(byTime(c.entries))  //排序  得到最先要执行的entry

 

        var timer *time.Timer

        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.

            timer = time.NewTimer(100000 * time.Hour)

        } else {

            timer = time.NewTimer(c.entries[0].Next.Sub(now))  //时间最近要执行Entry到现在的时间差 下面唤醒select

        }

 

        for {   //这个for有什么用???

            select {

            case now = <-timer.C:      //时间到  执行任务

                now = now.In(c.location)   //更新时间

                // Run every entry whose next time was less than now

                for _, e := range c.entries {

                    if e.Next.After(now) || e.Next.IsZero() {

                        break

                    }

                    go c.runWithRecovery(e.Job)  //执行任务

                    e.Prev = e.Next

                    e.Next = e.Schedule.Next(now)  //下一个要执行的时间

                }

 

            case newEntry := <-c.add:    //运行中添加Entry

                timer.Stop()

                now = c.now()

                newEntry.Next = newEntry.Schedule.Next(now)

                c.entries = append(c.entries, newEntry)

 

            case <-c.snapshot:   // 快照

                c.snapshot <- c.entrySnapshot()

                continue

 

            case <-c.stop:   //停止信号

                timer.Stop()

                return

            }

 

            break

        }

    }

}

 

 

这里就基本是调度的所有内容了,有些东西写在上面注释就不解释了,我能解释的,也就只有这么多了
下面对这个调度相关的东西做下笔记

entry.Schedule.Next(now)

这个函数主要调用了Schedule的Next方法,Schedule是一个接口,在前面我们知道,实际上在解析spec的时
候返回的变量是SpecSchedule类型,所以此处应该调用SpecSchedule的Next方法,这个方法就是上面说的
那个复杂不贴代码的方法,在网上找了个带注释的版本,反正就是得到这个entry下次执行的时间吧

 

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

func (s *SpecSchedule) Next(t time.Time) time.Time {

    // 秒级别的取整

    t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)

 

    // 判断一个字段是否被累加,如果是, 那么它的下一级别的字段需要归 0 。

    added := false

 

    //到未来的探寻不超过5年

    yearLimit := t.Year() + 5

    // 下一级别的字段累加到重置,需要重新累加上一级别的字段的时候的goto点

    // 比如要找每个月的31号的时候, 4月是符合月份字段的规定的,但是4月的没有31号。 遍历尽4月的每一天后,只能请求重新累加月份。

WRAP:

    if t.Year() > yearLimit {

        return time.Time{}

    }

    // 月

    for 1<

        // If we have to add a month, reset the other parts to 0.

        if !added {

            added = true

            // Otherwise, set the date at the beginning (since the current time is irrelevant).

            t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())

        }

        t = t.AddDate(0, 1, 0)

 

        // Wrapped around.

        if t.Month() == time.January {

            goto WRAP

        }

    }

 

    // 天 , 一次处理 天/月 和 天/周

    for !dayMatches(s, t) {

        if !added {

            added = true

            t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())

        }

        t = t.AddDate(0, 0, 1)

 

        if t.Day() == 1 {

            goto WRAP

        }

    }

    // 时

    for 1<

        if !added {

            added = true

            t = t.Truncate(time.Hour)

        }

        t = t.Add(1 * time.Hour)

 

        if t.Hour() == 0 {

            goto WRAP

        }

    }

    // 分

    for 1<

        if !added {

            added = true

            t = t.Truncate(time.Minute)

        }

        t = t.Add(1 * time.Minute)

 

        if t.Minute() == 0 {

            goto WRAP

        }

    }

    // 秒

    for 1<

        if !added {

            added = true

            t = t.Truncate(time.Second)

        }

        t = t.Add(1 * time.Second)

 

        if t.Second() == 0 {

            goto WRAP

        }

    }

 

    return t

}

//一次处理 天/月 和 天/周 。 如果两者中有任意, 那么必须同时符合另一个才算是匹配

func dayMatches(s *SpecSchedule, t time.Time) bool {

    var (

        domMatch bool = 1< 0

        dowMatch bool = 1< 0

    )

 

    if s.Dom&starBit > 0 || s.Dow&starBit > 0 {

        return domMatch && dowMatch

    }

    return domMatch || dowMatch

}

 

 

初学者的摸爬滚打,对标准库还是太不了解了,如有错误,敬请指正

参考链接:
http://blog.studygolang.com/2014/02/go_crontab/
http://blog.csdn.net/cchd0001/article/details/51076922

 

 

 

你可能感兴趣的:(golang)