← 返回博客首页

Cron 表达式深入解析:字段、特殊字符、常见模式与排错指南

什么是 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 关系 两者必须其一为 ?

最容易踩的坑

  1. 日和星期同时指定:标准 Cron 中两者是 OR 关系(满足任一即执行),Quartz 中则必须用 ? 明确忽略一个
  2. 星期编号不同:标准 Cron 的 07 都是周日;Quartz 的 1 是周日,7 是周六
  3. 秒字段:从标准 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 时间调度

各语言常用调度库

语言 / 平台 常用库 备注
Node.js node-croncron 5/6 字段,不支持 L/W/#
Python APSchedulercroniter croniter 只做表达式计算,不含调度
Java Quartzspring-task Quartz 支持 6/7 字段与 L/W/#
Go robfig/cron 默认 5 字段,可选开启秒级
Kubernetes CronJob 标准 5 字段,固定 UTC

分布式环境的额外注意

标准 Cron 只有单机语义。多实例部署时,同一条表达式会在每台机器上都跑一遍

  • 用分布式锁(Redis Lock、数据库唯一索引)把执行权收敛到一个实例
  • 或改用具备集群协调的调度框架(Quartz Cluster、xxl-job,或 K8s CronJob 配 concurrencyPolicy: Forbid
  • 任务本身仍然要幂等——锁只降低并发概率,不能替代幂等设计

在线验证工具

建议在部署前使用在线 Cron 表达式验证工具检查:

最佳实践

  1. 明确时区:始终在文档和配置中注明 cron 表达式使用的时区
  2. 避免边界时间:不要用 0 0 0 * * *(午夜零点),多服务同时触发会造成负载尖峰,建议加随机偏移
  3. 注释清晰:在配置文件中为每个 cron 表达式添加注释说明触发逻辑
  4. 幂等设计:任务逻辑应幂等,防止重复执行造成数据问题
  5. 设置超时和重试:防止任务卡死阻塞后续调度
  6. 监控告警:对关键定时任务设置执行失败告警
广告

常见问题

5 位和 6 位、7 位的 Cron 有什么区别?

标准 Unix cron 是 **5 位**(分 时 日 月 周)。Quartz(Java 生态)是 **6 或 7 位**,在最前面多了**秒**,最后可多选**年**。所以 `0 0 12 * * ?` 在 Quartz 里是每天 12:00:00,但拿去喂标准 cron 会被解析成「每月 0 分 12 时…」这种荒谬结果。**两者语法不兼容,迁移时务必逐条验证。** 用 [Cron 解析器](/cron-parser.html) 可以先看实际触发时刻再上线。

`*/5 * * * *` 到底是什么意思?

每 5 分钟执行一次。`*/n` 表示「在该字段的取值范围内,每 n 个单位触发一次」。注意它是**按字段边界整除**触发的:分钟的 `*/5` 会在第 0、5、10 … 55 分触发,而不是「从启动时刻起每 5 分钟」。另外日和周两个字段同时指定具体值时是**或**关系(满足任一即执行),不是且——这是新手最容易误解的一点。

定时任务没执行,先查哪几项?

按顺序查五项:① **时区**——容器默认 UTC,你以为的 09:00 实际是 17:00;② **环境变量**——cron 的执行环境与交互式 shell 不同,`PATH` 常常缺失,命令要写绝对路径;③ **脚本权限与换行符**——`.sh` 需要 `chmod +x`,Windows 编辑过的文件换行符会导致 `bad interpreter`;④ **日志**——cron 默认不输出到终端,要显式重定向到文件;⑤ **表达式本身**——先用解析工具验证触发时刻。

每月最后一天怎么表达?

标准 cron 没有「最后一天」语法,只能变通:① 列出所有可能的最后一天 —— `0 0 28-31 * *` 配合脚本里判断「明天是不是 1 号」;② 用 `@monthly`(等价于 `0 0 1 * *`,是每月 1 号不是最后一天);③ 换用支持 `L` 语法的调度器(Quartz 支持 `L` 表示最后一天)。**推荐方案①**,跨调度器兼容性最好。

← 返回博客首页