如何读懂 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 到 23 | 24 小时制,0 表示午夜 |
| 3 | 日 | 1 到 31 | |
| 4 | 月 | 1 到 12,或 JAN 到 DEC | |
| 5 | 星期 | 0 到 6,或 SUN 到 SAT | 0 表示星期日,多数 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 * * * 是合法的小时字段。
名称和快捷写法
月份和星期字段接受三个字母的名称,大小写无关,因此 JAN、jan 和 Jan 是同一个东西。经典的 crontab 文档写明名称不能用于范围和列表,不过很多现代实现确实接受 MON-FRI。如果你不确定是什么在运行你的任务,就用数字:1-5 在任何地方都没有歧义。
有几个快捷写法可以替代整个五字段表达式:
| 快捷写法 | 等价表达式 | 运行时间 |
|---|---|---|
@yearly 或 @annually | 0 0 1 1 * | 1 月 1 日午夜 |
@monthly | 0 0 1 * * | 每月 1 日午夜 |
@weekly | 0 0 * * 0 | 每周日午夜 |
@daily 或 @midnight | 0 0 * * * | 每天午夜 |
@hourly | 0 * * * * | 每个整点 |
@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 使用系统或用户的时区。夏令时调整时钟时,落在被跳过那一小时里的任务可能根本不运行,落在重复那一小时里的任务可能运行两次,具体取决于实现。必须每天运行一次的任务,最好避开时钟切换所在的凌晨时段。
拿不准的时候,把表达式当作一句话念回来,再看看它接下来几次实际会在什么时候触发。如果那些时间点不是你描述的,那就是表达式错了,而不是你读错了。