关于crontab格式的一些理解

业务有延迟任务需求,开始考虑使用 crontab实现。但调研之后发现 crontab对于实现某小时后执行某个操作这类需求并不方便,特此记录。

背景

crontab格式一般为 * * * * *,分别代表 minute, hour, day of month, month, year。指定某个时刻执行任务,表达式比较直观, 7 8 9 10 *表示 每年的10月9号8点7分执行一次*/5 * * * *表示 每5分钟执行一次

问题

如果要实现每隔2小时执行任务,第一反应会将表达式写为 * */2 * * *。在crontab解析工具中获取最近7次执行时间如下:

2022-01-15 16:22:00
2022-01-15 16:23:00
2022-01-15 16:24:00
2022-01-15 16:25:00
2022-01-15 16:26:00
2022-01-15 16:27:00
2022-01-15 16:28:00

得到的结果为每分钟执行一次,与预期不符。因为分钟域的策略与小时域策略冲突,会将小时域策略短路。为了实现预期目的,需在分钟域指定具体分钟数。改为 22 */2 * * *,假设当前时间为 16:39:30,查询到的近7次执行时间入下:

2022-01-15 18:22:00
2022-01-15 20:22:00
2022-01-15 22:22:00
2022-01-16 00:22:00
2022-01-16 02:22:00
2022-01-16 04:22:00
2022-01-16 06:22:00

执行间隔与预期相符。

但也存在一些场景,首次执行时间会跟预期出现偏差:

  1. 场景一
2022/01/15 16:57:00 cronStr: 57 */2 * * *
2022/01/15 16:57:00 base time: 2022-01-15 16:27:00.023912 +0800 CST m=-1799.999701853
2022/01/15 16:57:00 next time: 2022-01-15 16:57:00 +0800 CST
  1. 场景二
2022/01/15 17:09:57 cronStr: 9 */2 * * *
2022/01/15 17:09:57 base time: 2022-01-15 17:09:57.354229 +0800 CST m=+0.000260331
2022/01/15 17:09:57 next time: 2022-01-15 18:09:00 +0800 CST

场景一下第一次触发时刻为base time的半小时后,场景二下第一次触发时刻为1小时后。使用 crontab做指定间隔任务,首次执行时间是不固定的。

结论

经过多次测试,发现 crontab在指定执行间隔的场景下,执行时刻规律如下:

计算小时域执行时刻时,始终以0点作为基准。如果小时域表达式为 */2,则小时域计算得到的触发时刻列表为 2,4,6,8,10,12...18,20,22,0。如果表达式为 */7,结果为 7,14,21。即触发时刻列表为表达式 */n中n的整数倍(不一定包含0)。最近触发时刻则为列表中距离基准时刻最近的时刻。

参考代码

func testCron() {
	parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
	min := time.Now().Minute()
	cronStr := fmt.Sprintf("%d */7 * * *", min)
	log.Printf("cronStr: %s\n", cronStr)
	sched, err := parser.Parse(cronStr)
	if err != nil {
		log.Fatal(err)
	}
	m, _ := time.ParseDuration("-30m")
	baseTime := time.Now().Add(m)
	log.Printf("base time: %+v\n", baseTime)
	next := sched.Next(baseTime)
	log.Printf("next time: %+v\n", next)
}

参考

crontab只执行一次

crontab tool