返回博客

发布于 · 阅读约 3 分钟

从零开始学 cron 表达式:可视化教程

学会 POSIX 五字段 cron 语法、特殊字符、步长值、Quartz 扩展、AWS cron 的差异,以及十几个能直接粘进 crontab 的实战示例。

为什么 cron 至今无处不在

cron 是 Brian Kernighan 于 1979 年为 Version 7 Unix 写的程序。现代 Linux 系统仍在使用的版本——Paul Vixie 1987 年写的 Vixie cron——增加了环境变量、按用户的 crontab 和友好的 @reboot 简写,但几乎原封不动地保留了最初的五字段调度格式。正是这种稳定性,让一种诞生于万维网之前的语法,在 2026 年依然出现在 Kubernetes 清单、GitHub Actions 工作流、AWS EventBridge 规则和 Spring @Scheduled 注解里。

如果你学 cron 只靠从 Stack Overflow 复制行,迟早会出意外——每日备份变成每分钟跑一次,周末任务在周二触发,DST 切换时时间偏移。本文从基础讲起,再给出足够多的实战示例,让你一眼就能认出任何表达式的形状。

五字段格式

POSIX 的 crontab 行由调度表达式和命令组成。调度部分由空白分隔的恰好五个字段构成:分钟、小时、日、月、周。每个字段都接受数字、列表、范围、步长或通配符 *。

* * * * *  command-to-run
┬ ┬ ┬ ┬ ┬
│ │ │ │ │
│ │ │ │ └─── 周          (0-6,周日 = 0;多数实现里 7 也代表周日)
│ │ │ └───── 月          (1-12,或 JAN-DEC)
│ │ └─────── 日          (1-31)
│ └───────── 小时         (0-23)
└─────────── 分钟         (0-59)

特殊字符:* , - / 和它们的伙伴

五个字符承担了经典 cron 里几乎所有的语义。学会它们各自的含义后,大多数表达式读起来就像一句话。

  • * —— 该字段的全部取值。分钟字段为 * 表示「每分钟」。
  • , —— 离散值列表。分钟字段写 0,15,30,45 表示每小时四个固定时刻。
  • - —— 闭区间范围。周字段写 1-5 表示周一到周五。
  • / —— 步长,写法为 范围/步长 或 */步长。分钟字段写 */10 表示「从 0 开始每 10 分钟」。
  • L、W、# —— Quartz 扩展,不属于 POSIX。L = 最后,W = 最近的工作日,# = 当月第 n 个星期几。后文会讲。
0,30 * * * *      每小时的 :00 和 :30
*/5 * * * *       每 5 分钟(0、5、10、15...)
0 9-17 * * 1-5    周一到周五,09:00 到 17:00 每小时
0 0 1,15 * *      每月 1 日和 15 日的午夜
15 14 * * 0       每周日 14:15

步长值并不是你以为的那样

步长操作符 / 永远作用在一个范围上,即便范围是隐式的。分钟字段写 */15 等价于 0-59/15,会在 0、15、30、45 触发。所以分钟里写 */7 并不表示「从现在起每 7 分钟」,而是「0 到 59 中所有 7 的倍数」,也就是 0、7、14、21、28、35、42、49、56,然后跨越午夜前会留 4 分钟空档。如果你需要真正的滚动间隔,请用任务运行器或 systemd 计时器的 OnUnitActiveSec。

步长也可以从范围中部开始。分钟字段写 5-59/10 会在 5、15、25、35、45、55 触发。当你需要非对称调度又不想写一长串逗号列表时,把范围和步长结合起来。

*/15 * * * *      00、15、30、45  (正确)
*/7 * * * *       00、07、14、21、28、35、42、49、56,然后 4 分钟空档(多半不是你想要的)
5-59/10 * * * *   05、15、25、35、45、55
0 */4 * * *       00:00、04:00、08:00、12:00、16:00、20:00

日字段和周字段:那个 OR 规则

cron 最大的踩坑点:当日和周两个字段都被限制(都不是 *)时,Vixie cron 把它们当成逻辑 OR,而不是 AND。0 0 13 * 5 这一行表示每月 13 号的午夜「或」每周五——而不是只在「13 号且是周五」时触发。要做 AND,你只能限制其中一个字段、在脚本里再过滤另一个,或者用支持 ? 占位符(表示「不指定」)的调度器如 Quartz。

0 0 13 * 5        每月 13 号的午夜,或任意周五(Vixie cron 的 OR 规则)
0 0 * * 5         每周五午夜
0 0 13 * *        每月 13 号午夜
# Quartz: 0 0 0 13 * 5 ?  才会精确匹配「13 号且是周五」

十个直接复制的示例

把这张表打印出来贴在显示器旁。你以后写的多数生产 cron 行,都只是这些示例的微调。

* * * * *           每分钟
0 * * * *           每小时整点
*/15 * * * *        每 15 分钟
0 0 * * *           每天午夜(服务器本地时间)
0 9 * * 1-5         工作日 09:00
30 14 * * 1-5       工作日 14:30
0 9 * * 1           每周一 09:00
0 22 * * 0          每周日 22:00
0 0 1 * *           每月 1 日午夜
0 0 1 1 *           1 月 1 日午夜
0 3 * * 6           每周六 03:00(典型备份时段)
*/5 9-17 * * 1-5    工作日营业时间内每 5 分钟

命名简写

Vixie cron、GNU mcron 及其后代大多支持以 @ 开头的别名,对应常见调度。它们比五个字段更易读,也更不容易打错。

@yearly    等同于 0 0 1 1 *     (也叫 @annually)
@monthly   等同于 0 0 1 * *
@weekly    等同于 0 0 * * 0
@daily     等同于 0 0 * * *     (也叫 @midnight)
@hourly    等同于 0 * * * *
@reboot    系统启动时执行一次

Quartz:6 或 7 字段的表兄

Java 调度器——Quartz 本身、Spring 的 @Scheduled(cron = ...)以及一些云平台——使用另一种方言。Quartz 在前面加一个秒字段,可选地在末尾加一个年字段,总共 6 或 7 个字段。它还支持 L(最后)、W(最近工作日)、#(当月第 n 个星期几)以及 ? 占位符——表示「该字段不指定,日和周中只设其一」。

如果把 Quartz 表达式复制到 Linux 的 crontab,会被静默解析错,因为开头那个 0 在 Vixie cron 看来只是普通的分钟值。保存文件前,永远先确认你的调度器用的是哪种方言。

Quartz 字段:秒 分 时 日 月 周 [年]

0  0  12  *  *  ?           每天中午 12 点
0  15 10  ?  *  MON-FRI     工作日 10:15
0  0  0   L  *  ?           每月最后一天午夜
0  0  0   LW *  ?           每月最后一个工作日
0  0  9   ?  *  2#1         每月第一个周一 09:00
0  0  9   ?  *  6#3         每月第三个周五 09:00
0  0  0   1  *  ?  2026     仅 2026 年内,每月 1 日午夜

AWS 的 rate 与 cron 表达式

EventBridge、CloudWatch Events 和 Lambda 用的是接近 Quartz 的 cron 语法,但有两个重要差别:没有秒字段(共 6 个字段,不是 7 个),并且日和周字段必须有且只有一个为 ?,不能都为 *。AWS 同时提供 rate(value unit) 用于简单的间隔触发,可彻底回避 OR 陷阱。

AWS cron 字段:分 时 日 月 周 年

cron(0 12 * * ? *)        每天 12:00 UTC
cron(0/15 * * * ? *)      每 15 分钟
cron(0 9 ? * MON-FRI *)   工作日 09:00 UTC
cron(0 0 1 * ? *)         每月 1 日 UTC 午夜

rate(5 minutes)           每 5 分钟
rate(1 hour)              每小时
rate(7 days)              从规则创建起每 7 天

时区、夏令时和那些在 02:30 出问题的东西

Vixie cron 遵循系统时区(TZ 环境变量,或 /etc/localtime 所指的时区)。夏令时跳一小时向前时,落在那段空窗里的任务会被静默跳过;夏令时回拨时,被复制的那一小时内的任务在某些实现里会跑两次,另一些则只跑一次。两个安全的默认做法:让 cron 跑在 UTC,或者把任务安排在 01:00–04:00 区间之外。

云端调度器行为各异:EventBridge 永远按 UTC 跑;Kubernetes CronJob 自 v1.27 起支持 spec.timeZone 字段;Quartz 默认按 JVM 默认时区,除非你给 CronTrigger 显式传时区。每用一个新的调度器前都先看一眼文档,别假定上一个的约定还成立。

调试不触发的表达式

十之八九,表达式本身没错,是环境出了问题。在你怀疑调度之前,先走一遍这个清单。

  • 读 cron 日志:Debian/Ubuntu 上 grep CRON /var/log/syslog,systemd 主机上 journalctl -u cron。日志里有 cron 实际执行的命令行。
  • 确认是哪个用户。不带参数的 crontab -l 显示当前用户的 crontab;你关心的调度可能在 /etc/crontab、/etc/cron.d/* 或别的用户的 crontab 里。
  • 检查 PATH。cron 运行时 PATH 极简(通常是 /usr/bin:/bin)。要么用绝对路径,要么在 crontab 顶部设置 PATH=。
  • 检查 shell。cron 用 /bin/sh,不是你的登录 shell。任何 bash 专有的语法(进程替换、[[ ]])都需要显式用 bash -c 包一层。
  • 重定向输出。没有 > /var/log/myjob.log 2>&1,失败的任务会把邮件发给本地 root,你永远看不到。
  • 确认时区。在任务里同时执行 date && date -u。如果两者与你假设的时间不一致,那就是你的 bug。
# 一行更稳妥的 cron:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""

*/5 * * * *  /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1

什么时候 cron 不是合适的工具

cron 擅长固定的挂钟时间调度,对单机上的一行命令几乎无敌。但它不适合:跨多机的分布式协调(用队列、领导选举调度器或带 concurrencyPolicy: Forbid 的 Kubernetes CronJob);失败时的重试与退避(写脚本包一层或用工作流引擎);亚分钟粒度(用 systemd 计时器或事件驱动触发);以及依赖上一次跑完才能开始下一次的调度(cron 会在前一次还没结束时就再起一份)。早点意识到这些边界,能省掉很多凌晨 3 点的告警。

可视化构建与验证

如果你一个月写超过几行 cron,最有杠杆的习惯就是在提交前把每个表达式扔进解析器对一下。Multilities 提供了免费的 /tools/cron-builder,可把 5 或 6 字段表达式翻译成自然语言,并按本地时区显示接下来 10 次触发时间——这正是能抓住「日/周 OR 陷阱」和「*/7 步长意外」的交叉验证。再配上一个针对已知锚定日期的下次触发时间断言的单元测试,你的定时任务就能稳到几乎无聊——这才是你想要的状态。

试试这些工具