约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
crontab 文件、Linux 命令行、以及 Kubernetes CronJob 用的是 5 位:分 时 日 月 周。而 Java 的 Spring @Scheduled、Quartz、以及 Go 的部分库默认 6 位,最前面多一个「秒」。同样写 */5,前者在第 0 分钟触发(每 5 分钟一次),后者在每 5 秒的第 0 秒触发(日志里会刷出每秒 12 行)。新手上线前第一件事不是背字段表,而是确认调度器要几位。
| 位置 | 5 位(crontab) | 6 位(Spring/Quartz) | 取值范围 |
|---|---|---|---|
| 第 1 位 | 分钟 minute | 秒 second | 秒 0-59 / 分 0-59 |
| 第 2 位 | 小时 hour | 分钟 minute | 0-23 |
| 第 3 位 | 日 day-of-month | 小时 hour | 0-31 |
| 第 4 位 | 月 month | 日 day-of-month | 月 1-12 / 日 1-31 |
| 第 5 位 | 周 day-of-week | 月 month | 周 0-7(0 和 7 都是周日)/ 月 1-12 |
| 第 6 位 | 不存在 | 周 day-of-week | 0-7 |
*/5 * * * * 每 5 分钟 0 */2 * * * 每 2 小时(整点) 0 9-18 * * 1-5 工作日 9:00-18:59 每小时整点 0 0 1 * * 每月 1 号 00:00 @daily 等价于 0 0 * * * @reboot 开机后执行一次(常被误当服务管理器)
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 不支持这些宏,写过去会直接解析失败。
| 检查项 | 怎么验证 | 典型症状 |
|---|---|---|
| 服务是否装了 cron | systemctl 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 默认不保留任何输出 |
| 权限与 SELinux | ls -l 脚本、CAA 的最小权限 | 手动能跑,定时跑就 permission denied |
| 表达式位数不对 | 用解析器预览未来执行时间 | 5 位表达式被当 6 位解析,运行时间离谱 |
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>换算几次实际执行时刻,比凭感觉确认要可靠得多。
值得,但前提是你已经在用 systemd。timer 相比 cron 的实质提升有四点:一是 systemd 能记录任务的执行结果和退出码,journalctl 里直接查得到,不用自己重定向日志;二是可以写 OnUnitActiveSec= 这类相对间隔,不必凑 cron 的整点表达式;三是 Persistent=true 能在机器关机期间错过后补跑一次,补上 cron 最大的短板;四是能配合 Restart= 与资源限制做依赖编排。代价是配置样板明显变多,一段简单的每分钟任务在两种写法下的行数差三倍。如果机器上还有 sysvinit 脚本或者你只需要「每天跑一次」,继续用 crontab 也完全合理。
看调度器:crontab 文件、Linux 命令行、Kubernetes CronJob 是 5 位(分 时 日 月 周);Java 的 Spring @Scheduled 和 Quartz 是 6 位(多一个开头的秒)。写之前用 Cron 解析器预览未来几次执行时间,能立刻看出是不是被按错位数解析了。
没有区别,两者完全等价。但要注意它们都是按服务器时区触发,且不会因为错过时间而补跑——机器在触发点宕机,这一天的任务就直接没了。
按顺序查:cron 服务是否在跑(systemctl status crond)、表达式是否被正确解析、时区是否与你预期一致、命令是否用了绝对路径(cron 的 PATH 极简)、以及是否 cd 到了脚本所在目录。多数情况卡在第 3 或第 4 项。
这是 crontab 的固有行为,它不做并发控制。解决办法是在脚本开头做 pid 或锁文件排他检查,已经有实例在跑就直接退出;要更强的能力就换成 systemd timer 或用专门的调度器。