跳到主内容
d.devtul.fun
EN
运维 · 2026-09-22

Cron 表达式完全参考指南

Cron 作为 Unix 系统的默认定时任务调度器已经存在了四十年,到现在依然无处不在:备份脚本、报表生成、缓存预热、日志切割,背后都是它。但这个表达式语法是那种大多数工程师从网上抄一段、却从不真正吃透的东西。这篇指南,就是我想一直摆在终端旁边的那份参考。

五个字段,从左到右

标准 Cron 表达式是五个用空白分隔的字段:

┌── 分钟          (0–59)
│ ┌── 小时          (0–23)
│ │ ┌── 日          (1–31)
│ │ │ ┌── 月          (1–12,也可写 JAN–DEC)
│ │ │ │ ┌── 星期        (0–6,0 和 7 都表示周日)
│ │ │ │ │
* * * * *

从左到右读作"在哪些分钟、哪些小时、哪些日期、哪些月份、哪些星期"。默认情况下这五个字段是"与"的关系,一个时间点只有五个字段全部命中才会触发。

每个字段的取值范围

字段允许值注意
分钟0–59—
小时0–230 是午夜,23 是 23:00
日1–31会按当月实际天数截断
月1–12也接受 JAN–DEC 名称
星期0–60 和 7 都表示周日,也接受 SUN–SAT

每个特殊符号的含义

符号含义例子
*任意值分钟写 * = 每一分钟
,枚举1,15 = 1 号和 15 号
-闭区间范围9-17 = 9 到 17
/步长 / 间隔*/15 = 每 15 个单位
?"不指定"(仅 Quartz)用于日或星期其一
L最后(仅 Quartz)5L = 当月最后一个周五
W最近的工作日(仅 Quartz)15W = 离 15 号最近的工作日
#第几个星期几(仅 Quartz)2#1 = 第一个周一

最后四个 —— ?、L、W、# —— 在标准 Vixie Cron 里根本不存在,它们是 Quartz / Spring 调度体系特有的。如果你把它们直接贴进普通的 /etc/cron.d,要么报错,要么行为诡异。在从 Java 教程里抄表达式之前,先画清这条界线。

最容易踩的坑:日与星期的"或"

这是 Cron 里最反直觉的一条规则。当日和星期两个字段都被限制(都不为 *)时,大多数实现(包括 Vixie Cron 和几乎所有主流 Linux 发行版)会把它们当成"或",而不是"与"。

0 0 1 * 1

直觉会读成"每月 1 号、且是周一的零点",实际含义却是"每月 1 号,或者任意一个周一"。要表达"1 号且是周一",只能把其中一个字段留成 *,剩下的判断写进脚本里:

# 只在每月 1 号且是周一时运行
0 0 1 * *  [ "$(date +%u)" = "1" ] && /opt/report.sh

这条规则每年都会坑到一批人,尤其是账单、报表这类对日期极其敏感的任务。

常见表达式对照表

表达式触发时机
*/5 * * * *每 5 分钟
0 * * * *每小时整点
0 3 * * *每天凌晨 3:00
30 9 * * 1-5工作日 9:30
0 0 1 * *每月 1 号零点
0 0 * * 1每周一零点
0 0 1 1,4,7,10 *每季度首日
0 2 * * 6每周六 2:00

预设宏

很多 Cron 守护进程接受这些简写,用来替代五个字段:

@yearly   (或 @annually)  0 0 1 1 *
@monthly                  0 0 1 * *
@weekly                   0 0 * * 0
@daily    (或 @midnight)   0 0 * * *
@hourly                   0 * * * *
@reboot                   开机时执行一次

宏用起来方便,但并非所有平台都支持。生产环境依赖它们之前,先查一下你那台机器上 Cron 的手册。

时区:服务器时间 vs 业务时间

Cron 是按服务器本地时区来算的。同一份配置部署到两个不同时区,触发时间可能差好几个小时。在夏令时切换那天,落在被"跳过"的那个小时里的任务可能整天都不跑,而落在"重复"小时里的任务可能跑两遍。

如果时间精度是业务关键的,要么把主机时区统一成 UTC 并自己做好换算,要么改用能显式指定时区的调度器(systemd timer 支持 Persistent=true 和 Timezone=;Kubernetes 的 CronJob 默认按 UTC,除非你在容器里自己处理)。

标准 Cron 与 Quartz / Spring 六字段

Quartz 和 Spring 的 @Scheduled 调度在最前面多了一个秒字段,变成六个字段:

┌── 秒    (0–59)
│ ┌── 分钟  (0–59)
│ │ ┌── 小时  (0–23)
│ │ │ ┌── 日    (1–31)
│ │ │ │ ┌── 月    (1–12)
│ │ │ │ │ ┌── 星期  (0–6)
│ │ │ │ │ │
* * * * * *

所以 Spring 里的 0 0 3 * * * 表示 03:00:00,而系统 crontab 里的 0 3 * * * 表示 03:00。把一个五字段表达式原样塞进六字段位置,每个值都会整体右移一列,悄悄把排期改掉。秒字段同时也解锁了 ?、L、W、#,这些是普通 Cron 拒绝的。

漏跑与重入

如果任务该触发时机器是关机的,标准 Cron 不会事后补跑 —— 那个时间点就这么错过了(而带上 Persistent=true 的 systemd timer 行为不同,会在下次开机时补跑)。又因为默认没有锁,一个运行时间超过自身间隔的任务会自己和自己重叠。对耗时或高频的任务,要用锁文件或 flock 保护:

* * * * *  /usr/bin/flock -n /tmp/job.lock /opt/job.sh

地雷:永远不触发的表达式

因为日和星期是"或"的关系,有些表达式永远不可能命中:

0 0 31 2 *

二月从来没有 31 天,所以这个任务从出生起就是死的 —— 但它能干净地通过解析,安安静静地躺在那儿装无辜。另一种静默杀手是跨月范围导致没有任何日期能同时满足。新表达式上线前,务必核对一下它接下来几次的触发时间。

部署前先验证

最快的检查办法,是让工具把表达式读回成一句人话,并列出接下来几次触发时间。Cron 表达式工具 就能把你的字符串翻成一句说明、并展示未来的触发时刻,像 0 0 31 2 * 这种地雷、或者那个无意间产生的"或",在上线前就被揪出来了。

继续阅读