管理定时任务--cron
corn
是一个用于管理定时任务的库,用 Go 实现 Linux 中 crontab
这个命令的效果。除了 cron
以外,Go语言中还有另一个比较小巧、灵活的定时任务库,可以执行定时的、周期性的任务。但是它功能相对简单些,并且已经不维护了。如果有定时任务需求,还是建议使用cron
。
简单使用
1 | package main |
使用非常简单,创建cron
对象,这个对象用于管理定时任务。
调用cron
对象的AddFunc()
方法向管理器中添加定时任务。AddFunc()
接受两个参数,参数 1 以字符串形式指定触发时间规则,参数 2 是一个无参的函数,每次触发时调用。@every 1s
表示每秒触发一次,@every
后加一个时间间隔,表示每隔多长时间触发一次。例如@every 1h
表示每小时触发一次,@every 1m2s
表示每隔 1 分 2 秒触发一次。time.ParseDuration()
支持的格式都可以用在这里。
调用c.Start()
启动定时循环。
注意一点,因为c.Start()
启动一个新的 goroutine 做循环检测,我们在代码最后加了一行time.Sleep(time.Second * 5)
防止主 goroutine 退出。
运行效果,每隔 1s 输出一行字符串:
1 | $ go run main.go |
时间格式
与Linux 中crontab
命令相似,cron
库支持用 5 个空格分隔的域来表示时间。这 5 个域含义依次为:
Minutes
:分钟,取值范围[0-59]
,支持特殊字符* / , -
;Hours
:小时,取值范围[0-23]
,支持特殊字符* / , -
;Day of month
:每月的第几天,取值范围[1-31]
,支持特殊字符* / , - ?
;Month
:月,取值范围[1-12]
或者使用月份名字缩写[JAN-DEC]
,支持特殊字符* / , -
;Day of week
:周历,取值范围[0-6]
或名字缩写[JUN-SAT]
,支持特殊字符* / , - ?
。
注意,月份和周历名称都是不区分大小写的。
特殊字符含义如下:
*
:使用*
的域可以匹配任何值,例如将月份域(第 4 个)设置为*
,表示每个月;/
:用来指定范围的步长,例如将小时域(第 2 个)设置为3-59/15
表示第 3 分钟触发,以后每隔 15 分钟触发一次,因此第 2 次触发为第 18 分钟,第 3 次为 33 分钟。。。直到分钟大于 59;,
:用来列举一些离散的值和多个范围,例如将周历的域(第 5 个)设置为MON,WED,FRI
表示周一、三和五;-
:用来表示范围,例如将小时的域(第 1 个)设置为9-17
表示上午 9 点到下午 17 点(包括 9 和 17);?
:只能用在月历和周历的域中,用来代替*
,表示每月/周的任意一天。
了解规则之后,我们可以定义任意时间:
30 * * * *
:分钟域为 30,其他域都是*
表示任意。每小时的 30 分触发;30 3-6,20-23 * * *
:分钟域为 30,小时域的3-6,20-23
表示 3 点到 6 点和 20 点到 23 点。3,4,5,6,20,21,22,23 时的 30 分触发;0 0 1 1 *
:1(第 4 个) 月 1(第 3 个) 号的 0(第 2 个) 时 0(第 1 个) 分触发。
预定义时间规则
为了方便使用,cron
预定义了一些时间规则:
@yearly
:也可以写作@annually
,表示每年第一天的 0 点。等价于0 0 1 1 *
;@monthly
:表示每月第一天的 0 点。等价于0 0 1 * *
;@weekly
:表示每周第一天的 0 点,注意第一天为周日,即周六结束,周日开始的那个 0 点。等价于0 0 * * 0
;@daily
:也可以写作@midnight
,表示每天 0 点。等价于0 0 * * *
;@hourly
:表示每小时的开始。等价于0 * * * *
。
固定时间间隔
cron
支持固定时间间隔,格式为:
1 | @every <duration> |
含义为每隔duration
触发一次。<duration>
会调用time.ParseDuration()
函数解析,所以ParseDuration
支持的格式都可以。
时区
默认情况下,所有时间都是基于当前时区的。当然我们也可以指定时区,有 2 两种方式:
- 在时间字符串前面添加一个
CRON_TZ=
+ 具体时区,东京时区为Asia/Tokyo
,纽约时区为America/New_York
; - 创建
cron
对象时增加一个时区选项cron.WithLocation(location)
,location
为time.LoadLocation(zone)
加载的时区对象,zone
为具体的时区格式。或者调用已创建好的cron
对象的SetLocation()
方法设置时区。
使用示例:
1 | func main() { |
Job
接口
除了直接将无参函数作为回调外,cron
还支持Job
接口
1 | // cron.go |
我们定义一个实现接口Job
的结构:
1 | type GreetingJob struct { |
调用cron
对象的AddJob()
方法将GreetingJob
对象添加到定时管理器中:
1 | func main() { |
运行效果:
1 | $ go run main.go |
实际上AddFunc()
方法内部也调用了AddJob()
方法。首先,cron
基于func()
类型定义一个新的类型FuncJob
并实现Job
接口:
1 | // cron.go |
在AddFunc()
方法中,将传入的回调转为FuncJob
类型,然后调用AddJob()
方法:
1 | func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) { |
线程安全
cron
会创建一个新的 goroutine 来执行触发回调。如果这些回调需要并发访问一些资源、数据,我们需要显式地做同步。
自定义时间格式
cron
支持灵活的时间格式,如果默认的格式不能满足要求,我们可以自己定义时间格式。时间规则字符串需要cron.Parser
对象来解析。
我们先来看看默认的解析器是如何工作的。
首先定义各个域:
1 | // parser.go |
除了Minute/Hour/Dom(Day of month)/Month/Dow(Day of week)
外,还可以支持Second
。相对顺序都是固定的:
1 | // parser.go |
默认的时间格式使用 5 个域。
我们可以调用cron.NewParser()
创建自己的Parser
对象,以位格式传入使用哪些域,例如下面的Parser
使用 6 个域,支持Second
(秒):
1 | parser := cron.NewParser( |
调用cron.WithParser(parser)
创建一个选项传入构造函数cron.New()
,使用时就可以指定秒了:
1 | c := cron.New(cron.WithParser(parser)) |
这里时间格式必须使用 6 个域,顺序与上面的const
定义一致。
因为上面的时间格式太常见了,cron
定义了一个便捷的函数:
1 | // option.go |
注意Descriptor
表示对@every/@hour
等的支持。有了WithSeconds()
,我们不用手动创建Parser
对象了:
1 | c := cron.New(cron.WithSeconds()) |
选项
cron
对象创建使用了选项模式,我们前面已经介绍了 3 个选项:
WithLocation
:指定时区;WithParser
:使用自定义的解析器;WithSeconds
:让时间格式支持秒,实际上内部调用了WithParser
。
cron
还提供了另外两种选项:
WithLogger
:自定义Logger
;WithChain
:Job 包装器。
WithLogger
WithLogger
可以设置cron
内部使用我们自定义的Logger
:
1 | func main() { |
上面调用cron.VerbosPrintfLogger()
包装log.Logger
,这个logger
会详细记录cron
内部的调度过程。
默认的Logger
是什么样的:
1 | // logger.go |
WithChain
Job 包装器可以在执行实际的Job
前后添加一些逻辑:
- 捕获
panic
; - 如果
Job
上次运行还未结束,推迟本次执行; - 如果
Job
上次运行还未介绍,跳过本次执行; - 记录每个
Job
的执行情况。
我们可以将Chain
类比为 Web 处理器的中间件。实际上就是在Job
的执行逻辑外在封装一层逻辑。我们的封装逻辑需要写成一个函数,传入一个Job
类型,返回封装后的Job
。cron
为这种函数定义了一个类型JobWrapper
:
1 | // chain.go |
然后使用一个Chain
对象将这些JobWrapper
组合到一起:
1 | type Chain struct { |
调用Chain
对象的Then(job)
方法应用这些JobWrapper
,返回最终的Job
:
1 | func (c Chain) Then(j Job) Job { |
注意应用JobWrapper
的顺序。
总结
在本项目中其实并没有用到这么多与之相关的东西,当然还有更加详细的内容本文并没有记录。
本文就不放参考资料了,因为整篇博客都是照抄的别人的,原因是我破防了。