什么是 Cron 表达式?
Cron 表达式是一种用于定义定时任务执行时间的字符串,最初源于 Unix 的 cron 守护进程。如今它被广泛应用于各类调度系统——从 Linux crontab 到 Spring 的 @Scheduled、Quartz Scheduler、Kubernetes CronJob 等。
一个 Cron 表达式由 5 到 7 个字段组成,每个字段代表一个时间单位。
字段结构
标准 5 字段 Cron
* * * * *
│ │ │ │ │
│ │ │ │ └── 星期 (0-7,0 和 7 都表示周日)
│ │ │ └──── 月份 (1-12)
│ │ └────── 日 (1-31)
│ └──────── 时 (0-23)
└─────────── 分 (0-59)
Quartz 6/7 字段 Cron
Quartz Scheduler 使用 6 或 7 个字段,增加了秒和可选的年:
* * * * * * *
│ │ │ │ │ │ │
│ │ │ │ │ │ └── 年 (可选,1970-2099)
│ │ │ │ │ └──── 星期 (1-7,1=周日)
│ │ │ │ └────── 月份 (1-12)
│ │ │ └──────── 日 (1-31)
│ │ └────────── 时 (0-23)
│ └──────────── 分 (0-59)
└─────────────── 秒 (0-59)
字段对照表
| 字段 | 标准 Cron 范围 | Quartz 范围 | 允许的特殊字符 |
|---|---|---|---|
| 秒 | — | 0-59 | , - * / |
| 分 | 0-59 | 0-59 | , - * / |
| 时 | 0-23 | 0-23 | , - * / |
| 日 | 1-31 | 1-31 | , - * ? L W C |
| 月 | 1-12 或 JAN-DEC | 1-12 或 JAN-DEC | , - * / |
| 星期 | 0-7 或 SUN-SAT | 1-7 或 SUN-SAT | , - * ? L C # |
| 年 | — | 1970-2099 | , - * / |
特殊字符详解
* — 任意值
表示该字段的所有合法值。例如 * * * * * 表示每分钟执行。
, — 列举
指定多个离散值:
0 0 9,12,18 * * ? // 每天 9:00、12:00、18:00 执行
- — 范围
指定一个连续区间:
0 0 9-17 * * ? // 每天 9:00 到 17:00,每小时执行
/ — 步长
起始值/步长,从起始值开始每隔步长执行:
0 */15 * * * ? // 每 15 分钟执行一次
0 0/30 * * * ? // 每小时的 0 分和 30 分执行
? — 不指定(仅 Quartz)
仅用于日和星期字段,表示"不关心"。因为日和星期可能冲突,Quartz 要求两者不能同时使用 *,必须有一个是 ?。
0 0 12 ? * MON // 每周一中午 12:00(日字段用 ? 忽略)
0 0 12 1 * ? // 每月 1 号中午 12:00(星期字段用 ? 忽略)
L — 最后(Last,仅 Quartz)
- 在日字段:表示当月最后一天
- 在星期字段:表示当月最后一个指定的星期几
0 0 23 L * ? // 每月最后一天 23:00 执行
0 0 10 ? * 6L // 每月最后一个周五 10:00 执行
W — 最近工作日(仅 Quartz)
在日字段使用,表示最接近指定日期的工作日(周一至周五):
0 0 9 15W * ? // 每月最接近 15 号的工作日 9:00 执行
如果 15 号是周六,则在 14 号(周五)执行;如果是周日,则在 16 号(周一)执行。
# — 第 N 个星期几(仅 Quartz)
星期几#第几个:
0 0 10 ? * 2#1 // 每月第一个周一 10:00 执行
0 0 10 ? * 6#3 // 每月第三个周五 10:00 执行
常见调度模式
高频任务
| 表达式 | 说明 |
|---|---|
* * * * * |
每分钟 |
*/5 * * * * |
每 5 分钟 |
0 * * * * |
每小时整点 |
0 */2 * * * |
每 2 小时 |
日常任务
| 表达式 | 说明 |
|---|---|
0 0 * * * |
每天午夜 |
0 9 * * * |
每天上午 9 点 |
0 9 * * 1-5 |
工作日早上 9 点 |
0 22 * * 1-5 |
工作日晚上 10 点 |
周期任务
| 表达式 | 说明 |
|---|---|
0 0 1 * * |
每月 1 号 |
0 0 1 1 * |
每年 1 月 1 号 |
0 0 * * 0 |
每周日 |
0 0 1 */3 * |
每季度第一天 |
Quartz 专用模式
| 表达式 | 说明 |
|---|---|
0 0 12 ? * MON-FRI |
工作日中午(Quartz 格式) |
0 0 23 L * ? |
每月最后一天 23:00 |
0 0 9 15W * ? |
最接近 15 号的工作日 9:00 |
0 0 10 ? * 6#3 |
每月第三个周五 10:00 |
0 30 10-14 ? * MON,WED,FRI |
周一三五 10:30-14:30 每小时 |
Quartz vs 标准 Cron 核心差异
| 特性 | 标准 Cron | Quartz |
|---|---|---|
| 字段数 | 5 | 6 或 7 |
| 秒级精度 | ❌ | ✅ |
| 年字段 | ❌ | ✅(可选) |
? 字符 |
❌ | ✅(日/星期字段必填其一) |
L 字符 |
❌ | ✅ |
W 字符 |
❌ | ✅ |
# 字符 |
❌ | ✅ |
| 星期编码 | 0=周日, 7=周日 | 1=周日, 7=周六 |
| 日和星期关系 | 两者 OR 关系 | 两者必须其一为 ? |
最容易踩的坑
- 日和星期同时指定:标准 Cron 中两者是 OR 关系(满足任一即执行),Quartz 中则必须用
?明确忽略一个 - 星期编号不同:标准 Cron 的
0和7都是周日;Quartz 的1是周日,7是周六 - 秒字段:从标准 Cron 迁移到 Quartz 时容易忘记加秒字段
排错指南
1. 表达式不触发
检查清单:
- 时区是否正确?Quartz 默认使用 JVM 时区
- 日和星期字段是否冲突?Quartz 要求其一为
? - 步长语法是否正确?
*/15而非0-59/15(部分实现不支持后者) - 月份天数是否合法?
0 0 0 31 2 *永远不会触发(2 月没有 31 号)
2. 触发频率异常
# 意图:每 5 分钟执行
*/5 * * * * # ✅ 正确:0, 5, 10, 15...
0/5 * * * * # ⚠️ 部分系统等价,部分不等价
0-59/5 * * * * # ✅ 显式范围步长
3. 时区问题
- Linux cron 使用服务器本地时区
- Quartz 可以通过
CronTrigger设置时区 - Kubernetes CronJob 使用 UTC,需注意换算
// Quartz 指定时区
CronTrigger trigger = TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder
.cronSchedule("0 0 9 * * ?")
.inTimeZone(TimeZone.getTimeZone("Asia/Shanghai")))
.build();
4. 2 月 29 日问题
0 0 0 29 2 * 只在闰年触发。如果需要年度任务,建议用程序逻辑判断而非依赖 cron。
5. 冬令时/夏令时
在实行夏令时的时区,切换当天某些时间会重复或跳过。建议:
- 避免在 2:00-3:00 之间调度关键任务
- 使用 UTC 时间调度
在线验证工具
建议在部署前使用在线 Cron 表达式验证工具检查:
- crontab.guru — 标准 5 字段 Cron
- CronMaker — Quartz 7 字段 Cron
- FreeFormatter Cron Validator
最佳实践
- 明确时区:始终在文档和配置中注明 cron 表达式使用的时区
- 避免边界时间:不要用
0 0 0 * * *(午夜零点),多服务同时触发会造成负载尖峰,建议加随机偏移 - 注释清晰:在配置文件中为每个 cron 表达式添加注释说明触发逻辑
- 幂等设计:任务逻辑应幂等,防止重复执行造成数据问题
- 设置超时和重试:防止任务卡死阻塞后续调度
- 监控告警:对关键定时任务设置执行失败告警