如何读懂 cron 表达式

详解 cron 的五个字段,包括范围、步长、列表、名称和快捷写法,以及那条几乎人人都栽过跟头的“日”与“星期”规则。

cron 表达式是五个用空格分隔的值,从左到右依次读作分钟、小时、日、月、星期。只有当前时间与每一个字段都匹配时,任务才会运行。

30 4 * * 1
│  │ │ │ │
│  │ │ │ └── day of week (1 = Monday)
│  │ │ └──── month (any)
│  │ └────── day of month (any)
│  └──────── hour (4am)
└─────────── minute (30)

这一条读作“每周一 04:30”。

五个字段

#字段允许的值说明
1分钟0 到 59
2小时0 到 2324 小时制,0 表示午夜
31 到 31
41 到 12,或 JANDEC
5星期0 到 6,或 SUNSAT0 表示星期日,多数 cron 实现也接受 7

有些调度器采用不同的格式。大量 Java 软件使用的 Quartz 把秒放在最前面,还增加了一个可选的年份字段,并把星期按 1 到 7 编号、从星期日开始。如果一个表达式有六个或七个字段,先弄清它是为哪套系统写的,再去相信自己的解读。

运算符

一共四个,它们可以在同一个字段内组合使用。

符号名称示例含义
*任意* * * * *每天的每一分钟
,列表0 9,13,17 * * *在 09:00、13:00 和 17:00
-范围0 9-17 * * *从 09:00 到 17:00 每小时一次,含两端
/步长*/15 * * * *在 :00、:15、:30 和 :45

步长永远作用在一个范围之上。分钟字段里的 */15 表示“从 0 开始,每隔 15 取一个值,直到 59”。也可以给显式的范围加步长:5-30/10 得到 5、15 和 25,然后就停下,因为 35 已经越过了末端。

步长并不表示“从现在起每 15 分钟”,它选取的是钟面上的固定值。*/40 在 :00 和 :40 触发,随后只等 20 分钟就到了下一小时的 :00,因为计数在每个小时重新开始。列表里可以包含范围和步长,所以 0 0-6/2,12,18-23 * * * 是合法的小时字段。

名称和快捷写法

月份和星期字段接受三个字母的名称,大小写无关,因此 JANjanJan 是同一个东西。经典的 crontab 文档写明名称不能用于范围和列表,不过很多现代实现确实接受 MON-FRI。如果你不确定是什么在运行你的任务,就用数字:1-5 在任何地方都没有歧义。

有几个快捷写法可以替代整个五字段表达式:

快捷写法等价表达式运行时间
@yearly@annually0 0 1 1 *1 月 1 日午夜
@monthly0 0 1 * *每月 1 日午夜
@weekly0 0 * * 0每周日午夜
@daily@midnight0 0 * * *每天午夜
@hourly0 * * * *每个整点

@reboot 也存在,但它不是一个时间表:它在 cron 自身启动时把任务运行一次。

“日”与“星期”的陷阱

这是最容易咬人的一条。当日字段和星期字段同时被限定,也就是两者都不是 * 时,cron 会在任意一个匹配时运行任务,而不是在两者都匹配时。

0 0 13 * 5

你可能会把它读成“13 号星期五的午夜”。它的实际含义是“每月 13 日午夜,以及每个星期五午夜”:一年运行六十多次,而不是一两次。

你所期待的 AND 行为,只在这两个字段中有一个是 * 时才成立。所以 0 0 * * 5 是每个星期五,0 0 13 * * 是每月 13 日,两者都完全正常。同时限定这两个字段,才会触发 OR。

在五个字段里没有标准写法可以表达“13 号星期五”。常见的变通做法是把任务排在每月 13 日,再让任务在真正干活之前先检查一下星期几。

还有一个相关的细节:有些实现判断字段是否受限的方式,是看它字面上是不是以 * 开头。在这类实现下,星期字段里的 0-6 即便覆盖了每一天,也算作受限,于是悄悄启用了 OR 行为。想表达“任意”的时候就用 *

值得逐条检查的错误

  • * */2 * * * 会在每隔一小时的那个整小时里每分钟运行一次:一天 720 次。你想要的是 0 */2 * * *,那是 12 次。
  • 少写一个分钟字段会让后面所有内容左移一位,而且通常仍然能解析通过,于是你得到一个合法却完全错时的调度。
  • 0 0 31 * * 会无声地跳过没有 31 日的月份。
  • 除非调度器允许你指定时区,否则 cron 使用系统或用户的时区。夏令时调整时钟时,落在被跳过那一小时里的任务可能根本不运行,落在重复那一小时里的任务可能运行两次,具体取决于实现。必须每天运行一次的任务,最好避开时钟切换所在的凌晨时段。

拿不准的时候,把表达式当作一句话念回来,再看看它接下来几次实际会在什么时候触发。如果那些时间点不是你描述的,那就是表达式错了,而不是你读错了。