「连载十一」Cron定时任务

知识点

  • 完成定时任务的功能

本文目标

在实际的应用项目中,定时任务的使用是很常见的。你是否有过 Golang 如何做定时任务的疑问,莫非是轮询,在本文中我们将结合我们的项目讲述 Cron。

介绍

我们将使用 cron 这个包,它实现了 cron 规范解析器和任务运行器,简单来讲就是包含了定时任务所需的功能

Cron 表达式格式

字段名 是否必填 允许的值 允许的特殊字符
秒(Seconds) Yes 0-59 * / , -
分(Minutes) Yes 0-59 * / , -
时(Hours) Yes 0-23 * / , -
一个月中的某天(Day of month) Yes 1-31 * / , - ?
月(Month) Yes 1-12 or JAN-DEC * / , -
星期几(Day of week) Yes 0-6 or SUN-SAT * / , - ?

Cron 表达式表示一组时间,使用 6 个空格分隔的字段

可以留意到 Golang 的 Cron 比 Crontab 多了一个秒级,以后遇到秒级要求的时候就省事了

Cron 特殊字符

1、星号 ( * )

星号表示将匹配字段的所有值

2、斜线 ( / )

斜线用户 描述范围的增量,表现为 “N-MAX/x”,first-last/x 的形式,例如 3-59/15 表示此时的第三分钟和此后的每 15 分钟,到 59 分钟为止。即从 N 开始,使用增量直到该特定范围结束。它不会重复

3、逗号 ( , )

逗号用于分隔列表中的项目。例如,在 Day of week 使用“MON,WED,FRI”将意味着星期一,星期三和星期五

4、连字符 ( - )

连字符用于定义范围。例如,9 - 17 表示从上午 9 点到下午 5 点的每个小时

5、问号 ( ? )

不指定值,用于代替 “ * ”,类似 “ _ ” 的存在,不难理解

预定义的 Cron 时间表

输入 简述 相当于
@yearly (or @annually) 1 月 1 日午夜运行一次 0 0 0 1 1 *
@monthly 每个月的午夜,每个月的第一个月运行一次 0 0 0 1 * *
@weekly 每周一次,周日午夜运行一次 0 0 0 * * 0
@daily (or @midnight) 每天午夜运行一次 0 0 0 * * *
@hourly 每小时运行一次 0 0 * * * *

安装

1
$ go get -u github.com/robfig/cron

实践

在上一章节 Gin 实践 连载十 定制 GORM Callbacks 中,我们使用了 GORM 的回调实现了软删除,同时也引入了另外一个问题

就是我怎么硬删除,我什么时候硬删除?这个往往与业务场景有关系,大致为

  • 另外有一套硬删除接口
  • 定时任务清理(或转移、backup)无效数据

在这里我们选用第二种解决方案来进行实践

编写硬删除代码

打开 models 目录下的 tag.go、article.go 文件,分别添加以下代码

1、tag.go

1
2
3
4
5
func CleanAllTag() bool {
db.Unscoped().Where("deleted_on != ? ", 0).Delete(&Tag{})

return true
}

2、article.go

1
2
3
4
5
func CleanAllArticle() bool {
db.Unscoped().Where("deleted_on != ? ", 0).Delete(&Article{})

return true
}

注意硬删除要使用 Unscoped(),这是 GORM 的约定

编写 Cron

在 项目根目录下新建 cron.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
package main

import (
"time"
"log"

"github.com/robfig/cron"

"github.com/EDDYCJY/go-gin-example/models"
)

func main() {
log.Println("Starting...")

c := cron.New()
c.AddFunc("* * * * * *", func() {
log.Println("Run models.CleanAllTag...")
models.CleanAllTag()
})
c.AddFunc("* * * * * *", func() {
log.Println("Run models.CleanAllArticle...")
models.CleanAllArticle()
})

c.Start()

t1 := time.NewTimer(time.Second * 10)
for {
select {
case <-t1.C:
t1.Reset(time.Second * 10)
}
}
}

在这段程序中,我们做了如下的事情

cron.New()

会根据本地时间创建一个新(空白)的 Cron job runner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func New() *Cron {
return NewWithLocation(time.Now().Location())
}

// NewWithLocation returns a new Cron job runner.
func NewWithLocation(location *time.Location) *Cron {
return &Cron{
entries: nil,
add: make(chan *Entry),
stop: make(chan struct{}),
snapshot: make(chan []*Entry),
running: false,
ErrorLog: nil,
location: location,
}
}

c.AddFunc()

AddFunc 会向 Cron job runner 添加一个 func ,以按给定的时间表运行

1
2
3
4
5
6
7
8
func (c *Cron) AddJob(spec string, cmd Job) error {
schedule, err := Parse(spec)
if err != nil {
return err
}
c.Schedule(schedule, cmd)
return nil
}

会首先解析时间表,如果填写有问题会直接 err,无误则将 func 添加到 Schedule 队列中等待执行

1
2
3
4
5
6
7
8
9
10
11
12
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
}

3、c.Start()

在当前执行的程序中启动 Cron 调度程序。其实这里的主体是 goroutine + for + select + timer 的调度控制哦

1
2
3
4
5
6
7
func (c *Cron) Run() {
if c.running {
return
}
c.running = true
c.run()
}

time.NewTimer + for + select + t1.Reset

如果你是初学者,大概会有疑问,这是干嘛用的?

*(1)time.NewTimer *

会创建一个新的定时器,持续你设定的时间 d 后发送一个 channel 消息

(2)for + select

阻塞 select 等待 channel

(3)t1.Reset

会重置定时器,让它重新开始计时

注:本文适用于 “t.C 已经取走,可直接使用 Reset”。


总的来说,这段程序是为了阻塞主程序而编写的,希望你带着疑问来想,有没有别的办法呢?

有的,你直接 select{} 也可以完成这个需求 :)

验证

1
2
3
4
5
6
7
8
9
10
11
$ go run cron.go
2018/04/29 17:03:34 [info] replacing callback `gorm:update_time_stamp` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:56
2018/04/29 17:03:34 [info] replacing callback `gorm:update_time_stamp` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:57
2018/04/29 17:03:34 [info] replacing callback `gorm:delete` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:58
2018/04/29 17:03:34 Starting...
2018/04/29 17:03:35 Run models.CleanAllArticle...
2018/04/29 17:03:35 Run models.CleanAllTag...
2018/04/29 17:03:36 Run models.CleanAllArticle...
2018/04/29 17:03:36 Run models.CleanAllTag...
2018/04/29 17:03:37 Run models.CleanAllTag...
2018/04/29 17:03:37 Run models.CleanAllArticle...

检查输出日志正常,模拟已软删除的数据,定时任务工作 OK

小结

定时任务很常见,希望你通过本文能够熟知 Golang 怎么实现一个简单的定时任务调度管理

可以不依赖系统的 Crontab 设置,指不定哪一天就用上了呢

问题

如果你手动修改计算机的系统时间,是会导致定时任务错乱的,所以一般不要乱来。

参考

本系列示例代码

关于

修改记录

  • 第一版:2018 年 02 月 16 日发布文章
  • 第二版:2019 年 10 月 02 日修改文章

如果有任何疑问或错误,欢迎在 issues 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

我的公众号

image


免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com

推荐阅读:

文章标题:「连载十一」Cron定时任务

本文作者:知识铺

发布时间:2018-04-29, 12:00:00

最后更新:2020-04-19, 17:52:49

原始链接:https://blog.zshipu.com/2018/04/29/golang/go/gin/2018-04-29-cron/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏