Cron 表达式 5 位还是 6 位:从入门到排错的完整对照

同一个 */5 写法在 Linux 上是每 5 分钟、在 Java 里是每 5 秒——差别只在那一位可选的秒字段。本文讲清 5 位与 6 位两种方言的字段含义、四个容易写错的取值范围、@daily 这类简写,以及「任务不跑」时按什么顺序查。

约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

先确认你面对的是哪一种 Cron:差一位,含义差 60 倍

crontab 文件、Linux 命令行、以及 Kubernetes CronJob 用的是 5 位:分 时 日 月 周。而 Java 的 Spring @Scheduled、Quartz、以及 Go 的部分库默认 6 位,最前面多一个「秒」。同样写 */5,前者在第 0 分钟触发(每 5 分钟一次),后者在每 5 秒的第 0 秒触发(日志里会刷出每秒 12 行)。新手上线前第一件事不是背字段表,而是确认调度器要几位。

5 位与 6 位逐字段对照

位置5 位(crontab)6 位(Spring/Quartz)取值范围
第 1 位分钟 minute秒 second秒 0-59 / 分 0-59
第 2 位小时 hour分钟 minute0-23
第 3 位日 day-of-month小时 hour0-31
第 4 位月 month日 day-of-month月 1-12 / 日 1-31
第 5 位周 day-of-week月 month周 0-7(0 和 7 都是周日)/ 月 1-12
第 6 位不存在周 day-of-week0-7

五个必须记住的取值规则

  1. 周字段 0 和 7 都是周日,1 是周一——和「周一是一周之首」的直觉相反,这里周日才是 1 号前面的 0。
  2. 日字段和周字段是「或」关系,不是「与」:写 1 15 * * 5 表示每月 1 号或每周五,而不是两者同时满足。
  3. 越界的值会被拒绝而不是回绕:秒写 60、月写 13、周写 8 都直接报错,不会当成 0 或 1。
  4. */15 是步长,从该字段的最小值 0 起算,所以 */15 分钟是 0、15、30、45——如果你想从 5 分开始,写 5-59/15。
  5. 月和周不写名字时用数字 1-12 / 0-7;名字在部分实现里大小写敏感且不可移植,跨系统迁移时统一用数字最稳。
同义表达对照(Linux 5 位)
*/5 * * * *      每 5 分钟
0 */2 * * *     每 2 小时(整点)
0 9-18 * * 1-5   工作日 9:00-18:59 每小时整点
0 0 1 * *       每月 1 号 00:00
@daily          等价于 0 0 * * *
@reboot         开机后执行一次(常被误当服务管理器)
6 位写法(Spring / Quartz)
0 */5 * * * *     每 5 分钟(秒固定为 0)
0 0 2 * * ?      每天 02:00:00
0 15 10 ? * MON-FRI  工作日 10:15:00(Quartz 的 ? 表示不指定)
把表达式翻译成中文
30 4 * * 1  →  每周一 04:30 执行
0 0 */2 * *  →  每 2 天的 00:00(不是每两天,取决于何时启动)

简写宏:省事但可移植性差

crontab 支持 @yearly、@annually、@monthly、@weekly、@daily、@midnight、@hourly、@reboot 八个宏,含义都是字面意思。其中 @reboot 依赖系统启动时执行一次,很多人在这里误用——它只保证启动回调跑一次,不保证进程常驻、崩溃后重启、依赖就绪。要长期运行的任务应该交给 systemd timer(配合 Restart=always)或进程管理器,而不是 @reboot。Java 与 Quartz 不支持这些宏,写过去会直接解析失败。

任务不执行:按这个顺序查,不要瞎改

检查项怎么验证典型症状
服务是否装了 cronsystemctl status crond / cron编辑完 crontab 完全没反应
语法是否合法crontab -l 看原文,再看 /var/log/cron 或 journalctl -u crond编辑器里能存,但没有执行记录
时区timedatectl 看当前时区与是否 UTC执行时间整体差 8 小时,且不随夏令时变
环境变量在命令前写绝对路径,把 PATH 显式导出找不到命令、手动跑却能成功
HOME 与工作目录crontab 里写 cd /path && cmd脚本报「找不到文件」,但文件确实在
输出被丢弃末尾加 >> /var/log/job.log 2>&1脚本写 stderr,cron 默认不保留任何输出
权限与 SELinuxls -l 脚本、CAA 的最小权限手动能跑,定时跑就 permission denied
表达式位数不对用解析器预览未来执行时间5 位表达式被当 6 位解析,运行时间离谱

cron 的三个固有限制:并发、重叠与补跑

crontab 只管「什么时候启动」,不管「上一个跑完没有」。默认如果上一次执行超过一个周期还没结束,crond 会直接再启一个进程,于是任务重叠、数据重复写入,或者多个实例同时抢同一批文件。要避免这个,最简单的做法是在脚本开头做一次基于 pid 或锁文件的排他检查,失败就退出。更麻烦的是补跑:机器宕机期间错过的执行点,crond 不会补,直接跳过。所以凡是对完整性有要求的任务,都应该自己记录「上次处理到哪个位置/时间」,让下一次执行带着断点续跑的能力。

时区与夏令时:同样的表达式,不同的服务器差几小时

cron 用的是系统本地时区,不是 UTC。这意味着一台设成 Asia/Shanghai 的机器和一台设成 UTC 的机器,同一条 0 3 * * * 实际相差 8 小时;容器里如果没有设置 TZ 环境变量,通常退回 UTC,于是「每天凌晨 3 点跑批」在容器里变成了中午 11 点。更麻烦的是夏令时:实行夏令时的地区在切换那天的凌晨会出现「不存在的本地时间」和「重复两次的本地时间」,前者被直接跳过,后者触发两次。用 cron 做业务调度时,把时区显式写进配置、并用 <a class="faq-link" href="/tools/timestamp-converter/">时间戳转换工具</a>换算几次实际执行时刻,比凭感觉确认要可靠得多。

从 crontab 迁移到 systemd timer 值不值得

值得,但前提是你已经在用 systemd。timer 相比 cron 的实质提升有四点:一是 systemd 能记录任务的执行结果和退出码,journalctl 里直接查得到,不用自己重定向日志;二是可以写 OnUnitActiveSec= 这类相对间隔,不必凑 cron 的整点表达式;三是 Persistent=true 能在机器关机期间错过后补跑一次,补上 cron 最大的短板;四是能配合 Restart= 与资源限制做依赖编排。代价是配置样板明显变多,一段简单的每分钟任务在两种写法下的行数差三倍。如果机器上还有 sysvinit 脚本或者你只需要「每天跑一次」,继续用 crontab 也完全合理。

常见问题

crontab 表达式到底是 5 位还是 6 位?

看调度器:crontab 文件、Linux 命令行、Kubernetes CronJob 是 5 位(分 时 日 月 周);Java 的 Spring @Scheduled 和 Quartz 是 6 位(多一个开头的秒)。写之前用 Cron 解析器预览未来几次执行时间,能立刻看出是不是被按错位数解析了。

cron 里 0 0 * * * 和 @daily 有什么区别?

没有区别,两者完全等价。但要注意它们都是按服务器时区触发,且不会因为错过时间而补跑——机器在触发点宕机,这一天的任务就直接没了。

为什么 crontab 配好了完全没执行?

按顺序查:cron 服务是否在跑(systemctl status crond)、表达式是否被正确解析、时区是否与你预期一致、命令是否用了绝对路径(cron 的 PATH 极简)、以及是否 cd 到了脚本所在目录。多数情况卡在第 3 或第 4 项。

上一次任务还没跑完,下一次又启动了怎么办?

这是 crontab 的固有行为,它不做并发控制。解决办法是在脚本开头做 pid 或锁文件排他检查,已经有实例在跑就直接退出;要更强的能力就换成 systemd timer 或用专门的调度器。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件