1、背景
本文旨在走读cron源码来更好的理解时间表达式中各项字符串的意思。
本文基于 github.com/robfig/cron@v1.2.0 (公司项目用的这个版本,理解一下)
cron满足标准语义https://zh.wikipedia.org/wiki/Cron
2、简单的使用方式
func TestCron(t *testing.T){
// 新建实例
cronTimer := cron.New()
// 添加定时任务
_=cronTimer.AddFunc("0 0 0 0 * *",PrintSth)
// 开启定时任务
cronTimer.Start()
// 停止定时任务
cronTimer.Stop()
}
func PrintSth(){
fmt.Println("1")
}
3、New() *Cron
创建一个调度任务管理器实例
如下是执行New()方法后返回的结构体
如下是该结构体对外提供的方法
4、cron.AddFunc
添加调度任务
// AddFunc adds a func to the Cron to be run on the given schedule.
func (c *Cron) AddFunc(spec string, cmd func()) error {
return c.AddJob(spec, FuncJob(cmd))
}
// AddJob adds a Job to the Cron to be run on the given schedule.
func (c *Cron) AddJob(spec string, cmd Job) error {
// 解析cron表达式
schedule, err := Parse(spec)
if err != nil {
return err
}
c.Schedule(schedule, cmd)
return nil
}
// 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
}
5、cron.Start()
启动调度任务管理器
// 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()
}
// Run the cron scheduler, or no-op if already running.
func (c *Cron) Run() {
if c.running {
return
}
c.running = true
c.run()
}
// 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)
}
for {
// Determine the next entry to run.
sort.Sort(byTime(c.entries))
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))
}
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:
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
}
}
}
6、Parser.parse(spec string)
解析cron表达式为便于计算的数据结构
1、调度任务的数据结构
Schedule
:cron表达式解析后的数据结构
Next
: 该调度任务的下次执行时间
Prev
:该调度任务的上次执行时间
Job
:封装了调度任务实际要执行的方法
Schedule
的默认实现结构体,对应cron表达式中的6个表达式:
2、如何使用uint64来存储复杂的cron表达式
由常识已知:
Second
(秒)的范围是 [0, 59]
Minute
(分钟)的范围是 [0, 59]
Hour
(时)的范围是 [0, 23]
Dom
(Day of month 一个月中的第几天)的范围是 [1, 31]
Month
(月)的范围是 [1, 12]
Dow
(Day of week 一星期的第几天)的范围是**[0, 6]**,0为星期日
因此若使用长度>60的数组来存储cron表达式的各个值,即可满足存储需求。
这里robig/cron使用64位int类型的二进制表示各值是否存在。1
表示有值,0
表示无值,首bit位用来表示是否使用了*
举例:
表达式: 1,55 20-26/2 12 * * *
含义: 每天的 12:20:01 ,12:20:55, 12:22:01 ,12:22:55 ,12:24:01, 12:24:55 ,12:26:01 ,12:26:55 执行一次任务
存储结构:
表达式解析流程:
- 初始化用来存储的uint64数值为
0
- 以
,
符号 为分隔符 分割为一个数组,遍历此数组- 计算可取数值的最小值和最大值,若有
/
则已左侧字符串来取值- 若此值为
*
,最小值和最大值为默认,将最大bit位置为1
- 若此值为单个数值
x
, 最小值和最大值都置为x
- 若此值为范围值
x-y
,最小值置为x,最大值为y
- 若此值为
- 计算可取数值的最小值和最大值,若有
/
则已左侧字符串来取值- 若有
/
,则步长为右侧数值 - 若无
/
,则步长为1
- 若有
- 从最小值到最大值,每隔一个步长,对初始值进行
|
运算
- 计算可取数值的最小值和最大值,若有
7、Entry.Schedule.Next(t time.Time)
计算调度任务的下次执行时间
从上述流程可以总结出几点
- 下次调度时间计算顺序为 月->日->时->分→秒,且有年限制(5年),防止进入无限循环
- Dom和Dow满足https://zh.wikipedia.org/wiki/Cron的语义,即Dom和Dow互斥(OR的关系),有其中一个满足条件即可
8、v1.2.0较严重的bug
表象: dom和dow若使用*进行大于1的步长运算,那么它们俩将会变成(AND的关系),不符合标准cron的语义
解决方式: 升级到V3,相关issue: https://github.com/robfig/cron/issues/70
解析cron表达式的相关代码:
// 获取逗号分隔后的某个字符串对应的uint64值,秒/分/时/day of month/月/day of week 都会调用这个函数
// 例如 1-5/2 * * * * *的秒位 会被解析成 0101010
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 和end的值
}
// ...解析斜杆右侧值为步长 step
// ...校验解析结果是否合法
return getBits(start, end, step) | extra, 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
}
判断当天(Day)是否满足cron表达式:
// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
// s是配置好的表达式,t是当前时间
func dayMatches(s *SpecSchedule, t time.Time) bool {
var (
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
)
// 只有一个变量不为*,则满足此变量的条件就返回true
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
return domMatch && dowMatch
}
// 都指定变量,满足其中一个就返回true
return domMatch || dowMatch
}
这里存在一个bug,“只有一个变量不为*” 和"则满足此变量的条件就返回true"的执行行为不一致
即代码不满足 若s.Dow&starBit > 0成立 , dowMatch必须为true
解决方式:“只有一个变量不为*” → “只有一个变量没赋全部值”
如果有帮助到您,欢迎点赞
如果有任何疑问,欢迎评论