Cron 表达式写好了,任务就是不执行。日志没有报错,手动跑脚本没问题,但定时任务就是不动。这种情况大概率是 Cron 表达式或环境配置的问题。本文整理了 8 大常见原因和排查方法。
原因一:时区不一致
服务器时区是 UTC,但你的 Cron 表达式按北京时间写的。结果"每天 9 点执行"变成了北京时间 17 点执行。
# 查看服务器时区
$ timedatectl | grep "Time zone"
Time zone: UTC (UTC, +0000)
# 查看当前时间
$ date
Mon Aug 19 06:30:00 UTC 2026 # UTC 6:30 = 北京时间 14:30
排查方法:在脚本第一行打印时间,确认实际执行时间:
#!/bin/bash
echo "脚本执行时间: $(TZ='Asia/Shanghai' date)"
解决方案:
- 服务器时区改为
Asia/Shanghai - 或在 crontab 中指定时区(部分 cron 实现 支持
CRON_TZ=Asia/Shanghai) - Docker 容器中设置
TZ=Asia/Shanghai环境变量
Spring Boot 设置时区:
@Scheduled(cron = "0 0 9 * * ?", zone = "Asia/Shanghai")
public void execute() { }
原因二:日和周字段同时指定
Quartz 和 Spring 要求日和周字段不能同时为 *,必须有一个用 ?:
// 错误 — 日和周同时为 *,Quartz/Spring 会报错
@Scheduled(cron = "0 0 0 * * *")
// 正确 — 周字段用 ?
@Scheduled(cron = "0 0 0 * * ?")
如果你写的是 0 0 0 15 * MON(每月 15 号且是周一),这种"且"的语义在 Cron 中不支持。日和周同时指定具体值时,是"或"的关系——15 号或周一都会执行。
原因三:0 和 7 的周日歧义
# crontab — 0 表示周日
0 0 * * 0 # 每周日执行
# Quartz — 1 表示周日,0 是无效值
# 0 0 0 ? * 0 # 错误!Quartz 周字段范围是 1-7
# Spring — 0 和 7 都表示周日
@Scheduled(cron = "0 0 0 ? * 0") # 正确,周日
@Scheduled(cron = "0 0 0 ? * 7") # 正确,周日
排查方法:确认你用的框架的周字段编码。用英文缩写 SUN、MON 可以彻底避免这个问题。
原因四:/ 步长的起始值陷阱
*/5 看起来是"每 5 分钟",但 5/15 不一定是"从 5 开始每 15 分钟"——取决于实现。
# 正确 — 从 0 开始每 5 分钟
*/5 * * * *
# 从第 5 分钟开始每 15 分钟 → 5, 20, 35, 50
5/15 * * * *
# 常见误解:以为 */5 等价于 1/5
# 实际 */5 = 0/5 = 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55
在 Quartz 中,1/3 表示从第 1 秒开始每 3 秒:1, 4, 7, 10, …。如果你期望的是"从第 0 秒开始每 3 秒"(0, 3, 6, 9…),应该写 0/3 或 */3。
原因五:月末日期不存在
# 2 月没有 30 号和 31 号
0 0 0 30 * * # 2 月不执行!
0 0 0 31 * * # 2、4、6、9、11 月不执行
0 0 0 30 * * 在 2 月不会执行,因为 2 月最多 28/29 天。如果需要月末执行,用 L:
// Spring/Quartz
@Scheduled(cron = "0 0 0 L * ?") // 每月最后一天,自动适配
Unix crontab 不支持 L,只能用脚本判断:
0 0 0 28-31 * * [ "$(date -d '+1 day' +%d)" = "01" ] && /path/to/script.sh
原因六:crontab 用户权限
crontab 分用户级别和系统级别:
# 用户级 crontab — 当前用户执行
$ crontab -e
0 2 * * * /home/user/backup.sh
# 系统级 crontab — root 执行
# /etc/crontab 或 /etc/cron.d/
0 2 * * * root /opt/scripts/backup.sh
用户级 crontab 的脚本以当前用户权限执行。如果脚本需要 root 权限(如操作 /var/log),必须放在系统级 crontab 中,或用 sudo 配置免密执行。
排查方法:
# 查看 crontab 日志
$ grep CRON /var/log/syslog
# 查看特定用户的 crontab
$ sudo crontab -u root -l
$ sudo crontab -u www-data -l
原因七:环境变量缺失
crontab 执行的环境与 shell 不同。你在终端里能跑的脚本,在 crontab 里可能因为找不到命令而失败。
# crontab 的 PATH 很精简
# 通常只有 /usr/bin:/bin
# 你安装的 node/python/go 可能不在 PATH 中
排查方法:在脚本开头显式设置 PATH:
#!/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/opt/node/bin
export PATH
# 或者用绝对路径
/opt/node/bin/node /opt/scripts/cronjob.js
也可以在 crontab 文件顶部设置:
PATH=/usr/local/bin:/usr/bin:/bin:/opt/node/bin
0 2 * * * /opt/scripts/backup.sh
原因八:秒字段遗漏
从 Unix crontab 迁移到 Spring/Quartz 时,容易忘记补秒字段:
# Unix crontab — 5 段,正确
30 9 * * 1-5
// Spring — 直接复制,6 段格式少一段
@Scheduled(cron = "30 9 * * 1-5") // 错误!Spring 需要 6 段
// 正确 — 前面补秒
@Scheduled(cron = "0 30 9 * * ?")
少一段会导致 Spring 启动时报 IllegalArgumentException,错误信息提示字段数不匹配。
排查清单
| 排查项 | 检查方法 |
|---|---|
| 时区 | 脚本内打印 date,对比预期时间 |
| 日周互斥 | 确认 Quartz/Spring 中日和周有一个是 ? |
| 周编码 | 用 SUN/MON 缩写替代数字 |
| 步长起始 | */5 从 0 开始,不是 1 |
| 月末日期 | 2 月没有 30/31 号,用 L 或脚本判断 |
| 用户权限 | grep CRON /var/log/syslog 查看执行日志 |
| 环境变量 | 脚本开头设置 PATH,或用绝对路径 |
| 字段数 | Spring/Quartz 是 6 段,Unix 是 5 段 |
在线调试
排查问题时,需要反复验证 Cron 表达式。可以在 盘子工具站 Cron 表达式生成器 上做这种验证——输入表达式后,校验功能会指出语法错误和出错字段,比如 0 0 0 * * * 会提示"日和周不能同时为*“。中文解释帮你确认语义是否正确——如果解释显示"每天执行"但你期望的是"工作日执行”,说明表达式写错了。触发时间预览最实用——输入 0 0 0 30 * ? 后看触发时间列表,2 月的日期直接消失,一眼就能发现问题。


建议把上面排查清单里的每个错误示例粘贴到工具里,观察工具的校验提示和中文解释,加深对各种坑的理解。

864

被折叠的 条评论
为什么被折叠?



