cron库源码剖析
文章目录
Cron表达式
基本cron格式:
|
|
注:
- 在“星期域”(第五个域),0和7都被视为星期日。
- 不很直观的用法:如果日期和星期同时被设置,那么其中的一个条件被满足时,指令便会被运行。
- 前5个域称之分时日月周,可方便个人记忆。
从第六个域起,指明要运行的命令。
特殊符号说明:
-
星号(*) 表示 cron 表达式能匹配该字段的所有值。如在第5个字段使用星号(month),表示每个月
-
斜线(/) 表示增长间隔,如第1个字段(minutes) 值是 3-59/15,表示每小时的第3分钟开始执行一次,之后每隔 15 分钟执行一次(即 3、18、33、48 这些时间点执行),这里也可以表示为:3/15
-
逗号(,) 用于枚举值,如第6个字段值是 MON,WED,FRI,表示 星期一、三、五 执行
-
连字号(-) 表示一个范围,如第3个字段的值为 9-17 表示 9am 到 5pm 直接每个小时(包括9和17)
-
问号(?) 只用于 日(Day of month) 和 星期(Day of week),表示不指定值,可以用于代替 *
总结:
预定义模式:
举例:
每分钟执行一次命令:
|
|
每小时的第2和第10分钟执行:
|
|
每天上午9点到12点的第2和第10分钟执行:
|
|
每隔两天的上午9点到12点的第2和第10分钟执行:
|
|
每周一上午9点到12点的第2和第10分钟执行:
|
|
代码使用
|
|
在这段程序中,我们做了如下的事情
-
cron.New() 会根据本地时间创建一个新(空白)的 Cron job runner
-
c.AddFunc()
AddFunc 会向 Cron job runner 添加一个 func ,以按给定的时间表运行.首先解析时间表,如果填写有问题会直接 err,无误则将 func 添加到 Schedule 队列中等待执行
-
c.Start()
在当前执行的程序中启动 Cron 调度程序。其实这里的主体是 goroutine + for + select + timer 的调度控制
设计思路
基本类型
Cron
cron:包含一系列要执行的实体;支持暂停【stop】;添加实体等
|
|
注意:
- Cron 结构没有导出任何成员。
- 有一个成员 stop,类型是 struct{},即空结构体。
Entry
Entry:调度实体
|
|
Job
Job:每一个实体包含一个需要运行的Job
这是一个接口,只有一个方法:run
|
|
由于 Entity 中需要 Job 类型,因此,我们希望定期运行的任务,就需要实现 Job 接口。同时,由于 Job接口只有一个无参数无返回值的方法,为了使用方便,作者提供了一个类型:
|
|
它通过简单的实现 Run() 方法来实现 Job 接口,这样,任何无参数无返回值的函数,通过强制类型转换为 FuncJob,就可以当作 Job 来使用了,AddFunc 方法 就是这么做的。所以需要修改带参数功能的job时从此处下手
Schedule
Schedule:每个实体包含一个调度器(Schedule)
负责调度 Job 的执行。它也是一个接口。
|
|
Schedule 的具体实现通过解析 Cron 表达式得到。
库中提供了 Schedule 的两个具体实现,分别是 SpecSchedule 和 ConstantDelaySchedule。
SpecSchedule
|
|
从开始介绍的 Cron 表达式可以容易得知各个字段的意思,同时,对各种表达式的解析也会最终得到一个 SpecSchedule 的实例。库中的 Parse 返回的其实就是 SpecSchedule 的实例(当然也就实现了 Schedule 接口)。
看了上面的东西肯定有人疑惑为什么秒分时这些都是定义了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表达式的方法:
|
|
ConstantDelaySchedule
|
|
这是一个简单的循环调度器,如:每 5 分钟。注意,最小单位是秒,不能比秒还小,比如 毫秒。
通过 Every 函数可以获取该类型的实例,如:
|
|
得到的是一个每 5 秒执行一次的调度器。
Cron
实例化
|
|
可见实例化时,成员使用的基本是默认值;
成员方法
|
|
AddFunc
从AddFunc函数说起带参数任务的实现
|
|
AddFunc 含有两个参数,第一个是 cron表达式,这个不解释,第二个是func()类型参数cmd 即无参数无返回类型函数,下一步中直接将此参数强制转换为FuncJob类型,并调用AddJob函数
FuncJob类型:
|
|
由上述代码可知FuncJob为自定义类型,真实类型为 func(),此类型实现了一个Run()方法
AddJob
首先 AddJob 函数的传入参数为一个string类型的cron表达式和一个Job类型的cmd参数,但在AddFunc函数中,我们传入的第二个参数为FuncJob类型,所以Job类型应该是一个接口,在解析了cron表达式无错误以后,调用Schedule方法将cmd添加进了调度器
Job 类型:
|
|
由此可知,Job是带有一个Run方法的接口类型,经过代码分析可以指定,cron定时调度时间到达时,将调用此方法,也就是意味着,任何实现了Run方法的实例,都可以作为AddJob函数的cmd参数,而Run方法所实现的内容就是你定时调度所需执行的任务(AddFunc函数只能添加无参数无返回的任务,太鸡肋了),接下来我们就来实现一个带参数的任务添加
|
|
Parse
|
|
该函数主要是将cron表达式映射为“Second, Minute, Hour, Dom, Month, Dow”6个时间维度的结构体SpecSchedule。
Schedule
接下来看Cron类型的Schedule方法
|
|
这个比较好理解,根据schedule和cmd参数构建了一个Entry变量,并且将这个变量添加进Cron的entries中
只不过在没有运行的时候直接添加,运行的时候通过chan添加.
Start
调度的开始实施是从Cron.Start()函数开始的
|
|
东西很少,就是开了一个routine执行任务,这里cron还提供了一个使用当前routine执行的方法Run(),
|
|
先不管这些,接下来重点到run()方法
|
|
进入该函数,首先遍历所有任务,找到所有任务下一个要执行的时间。然后进入外层for循环,对于各个任务按照执行时间进行排序,保证离当前时间最近的先执行。再对任务列表进行判定,是否有任务如果没有,则休眠,否则初始化一个timer。
里层的for循环才是重头戏,下面主要分析这个for循环里面的任务加入和执行。
在此之前,需要了解下go标准库的timer:
- timer用于指定在某个时间间隔后,调用函数或者表达式。
- 使用NewTimer就可以创建一个Timer,在指定时间间隔到达后,可以通过<-timer.C接收值。
有了这个背景之后,我们再来看run函数的里层for循环。
接收到c.add信道
|
|
将timer停掉,清除设置的定时功能,并以当前时间点为起点,设置添加任务的下一次执行时间,并添加到entries任务队列中。
接收到timer.C信道
|
|
当定时任务到点后,time.C就会接收到值,并新开协程执行真正需要执行的Job,之后再更新下一个要执行的任务列表。
我们进入startJob函数,该函数从函数名就可以看出,即使出现panic也可以重新recovery,保证其他任务不受影响。
|
|
追根溯源,我们发现真正执行Job的是j.Run()的执行。进入这个Run函数的实现,我们看到
|
|
没错,我们要执行的任务一直从AddFunc一直往下传递,直到这里,我们通过调用Run函数,将包装的FuncJob类型的函数通过f()的形式进行执行。
Stop
|
|
entry
Schedule.Next
这个函数主要调用了Schedule的Next方法,Schedule是一个接口,在前面我们知道,实际上在解析spec的时 候返回的变量是SpecSchedule类型,所以此处应该调用SpecSchedule的Next方法,这个方法就是上面说的 那个复杂不贴代码的方法,在网上找了个带注释的版本,反正就是得到这个entry下次执行的时间吧
|
|
参考: https://segmentfault.com/a/1190000014666453 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/ https://juejin.im/post/5d3d79b9518825378f6cc6df
文章作者 Forz
上次更新 2019-10-15